<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="https://mzrn.sh/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mzrn.sh/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2026-05-25T18:46:16+00:00</updated><id>https://mzrn.sh/feed.xml</id><title type="html">mzrnsh</title><subtitle>Personal website of Giorgi Mezurnishvili, a freelance web developer &amp; indie maker. I share about the tech I use and the products I build with it.
</subtitle><author><name>mzrnsh</name></author><entry><title type="html">How to use Tailwind CSS with Jekyll on GitHub Pages</title><link href="https://mzrn.sh/2023/10/26/how-to-use-tailwind-css-with-jekyll-on-github-pages/" rel="alternate" type="text/html" title="How to use Tailwind CSS with Jekyll on GitHub Pages" /><published>2023-10-26T05:39:23+00:00</published><updated>2023-10-26T05:39:23+00:00</updated><id>https://mzrn.sh/2023/10/26/how-to-use-tailwind-css-with-jekyll-on-github-pages</id><content type="html" xml:base="https://mzrn.sh/2023/10/26/how-to-use-tailwind-css-with-jekyll-on-github-pages/"><![CDATA[<p>You are likely here because your site is built with Jekyll, styled with Tailwind CSS, and hosted on GitHub Pages. That combination doesn’t quite work out of box. Tailwind CSS requires PostCSS to compile and while Jekyll has a PostCSS plugin, it’s not currently <a href="https://pages.github.com/versions/">whitelisted</a>, meaning Tailwind CSS won’t be included in the default build produced by GitHub Pages. Let’s fix this via GitHub Actions.</p>

<h2 id="prerequisite-a-jekyll--tailwind-css-site-that-works-locally">Prerequisite: a Jekyll + Tailwind CSS site that works locally</h2>

<p>This tutorial assumes you already have a working Jekyll + Tailwind CSS website that’s successfully built locally, but when you try to deploy it to GitHub Pages, styles are not applied.</p>

<p>If that’s not true and you’re looking for general help on how to use Tailwind CSS with Jekyll, I documented the steps in <a href="https://mzrn.sh/2022/04/09/starting-a-blank-jekyll-site-with-tailwind-css-in-2022/">another article</a>.</p>

<h2 id="step-1-update-the-lockfile">Step 1: update the lockfile</h2>

<p>Open your <code class="language-plaintext highlighter-rouge">Gemfile.lock</code>. Do you see <code class="language-plaintext highlighter-rouge">x86_64-linux</code> under <code class="language-plaintext highlighter-rouge">PLATFORMS</code>? If yes, all good. If not, run this command in your terminal:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle lock <span class="nt">--add-platform</span> x86_64-linux
</code></pre></div></div>

<h2 id="step-2-confirm-jekyll-config-is-correct">Step 2: confirm Jekyll config is correct</h2>

<p>This step is technically not related to this tutorial, but it’s quite possible that someone might dismiss a working setup as broken because of a wrong stylesheet link, so let’s include it anyway.</p>

<p>Will you be using a custom domain? Great, your <code class="language-plaintext highlighter-rouge">_config.yml</code> should start like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _config.yml</span>

<span class="na">url</span><span class="pi">:</span> <span class="s1">'</span><span class="s">https://your-domain.com'</span>
<span class="na">baseurl</span><span class="pi">:</span> <span class="s1">'</span><span class="s">'</span>

<span class="c1"># ..rest of the config</span>
</code></pre></div></div>

<p>Planning to go with <strong>your-name.github.io</strong> option? Nice, just make sure your <code class="language-plaintext highlighter-rouge">_config.yml</code> starts with the following:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _config.yml</span>

<span class="na">url</span><span class="pi">:</span> <span class="s1">'</span><span class="s">https://your-name.github.io'</span>
<span class="na">baseurl</span><span class="pi">:</span> <span class="s1">'</span><span class="s">your-repo-name'</span>

<span class="c1"># ..rest of the config</span>
</code></pre></div></div>

<h2 id="step-3-create-a-github-workflow">Step 3: create a GitHub Workflow</h2>

<p>Create a new YAML file in <code class="language-plaintext highlighter-rouge">.github/workflows/</code> directory. Since it’s all about GitHub Pages, let’s call it <code class="language-plaintext highlighter-rouge">github-pages.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/github-pages.yml</span>

<span class="na">name</span><span class="pi">:</span> <span class="s">Build and deploy this site to GitHub Pages</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">github-pages</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v2</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">ruby/setup-ruby@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">ruby-version</span><span class="pi">:</span> <span class="m">3.1</span>
          <span class="na">bundler-cache</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Setup Node</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-node@v2</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">node-version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">18'</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">npm install</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build site</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">limjh16/jekyll-action-ts@v2</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">enable_cache</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">peaceiris/actions-gh-pages@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">github_token</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">publish_dir</span><span class="pi">:</span> <span class="s">./_site</span>
</code></pre></div></div>

<p>You can now commit this file and push this and all previous changes to GitHub.</p>

<p>Since your repository now includes the above workflow, pushing to the <code class="language-plaintext highlighter-rouge">main</code> branch will trigger a custom build instead of a standard one, and the generated <code class="language-plaintext highlighter-rouge">_site</code> directory will be pushed to the <code class="language-plaintext highlighter-rouge">gh-pages</code> branch.</p>

<p>A couple notes about that branch:</p>

<ol>
  <li>It will be created and updated automatically by the workflow. You don’t need to create it manually now and you won’t ever need to push to it directly in future.</li>
  <li>There’s nothing special about the name <code class="language-plaintext highlighter-rouge">gh-pages</code> [any more]: GitHub Pages can now work with <em>any</em> branch. But the GitHub action <code class="language-plaintext highlighter-rouge">peaceiris/actions-gh-pages</code> uses that name, and since our workflow relies on it, that’s what we’ll use.</li>
</ol>

<h2 id="step-4-configure-github-pages">Step 4: configure GitHub Pages</h2>

<p>Open your repository on GitHub and navigate to Settings &gt; Pages. In the <strong>Build and deployment</strong> section there are two dropdown controls: <strong>Source</strong> and <strong>Branch</strong>. Make sure their values are set to <strong>Deploy from a branch</strong> and <strong>gh-pages</strong> respectively.</p>

<p>It may seem confusing that we are <em>not</em> selecting the <strong>GitHub Actions</strong> option in <strong>Source</strong>, but we don’t need to: as far as GitHub Pages is concerned we are telling it to deploy a normal branch that contains a pre-built static website. It doesn’t even know (or care) that the website was built with Jekyll, or that it was styled via Tailwind CSS.</p>

<p><img src="/assets/uploads/gh-pages_settings.png" alt="GitHub Pages Settings" /></p>

<p>Does your screen look like that? 👆</p>

<p>Then, that should be it! 🥳 If I didn’t miss anything writing this tutorial, and you didn’t miss anything reading it, your Jekyll website should now look the same on GitHub Pages as it does on your machine.</p>]]></content><author><name>mzrnsh</name></author><category term="jekyll" /><summary type="html"><![CDATA[You are likely here because your site is built with Jekyll, styled with Tailwind CSS, and hosted on GitHub Pages. That combination doesn’t quite work out of box. Tailwind CSS requires PostCSS to compile and while Jekyll has a PostCSS plugin, it’s not currently whitelisted, meaning Tailwind CSS won’t be included in the default build produced by GitHub Pages. Let’s fix this via GitHub Actions.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mzrn.sh/assets/uploads/tailwind_gh-pages.png" /><media:content medium="image" url="https://mzrn.sh/assets/uploads/tailwind_gh-pages.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Never include user input-text in welcome emails</title><link href="https://mzrn.sh/2023/03/03/never-include-user-input-text-in-welcome-emails/" rel="alternate" type="text/html" title="Never include user input-text in welcome emails" /><published>2023-03-03T17:29:45+00:00</published><updated>2023-03-03T17:29:45+00:00</updated><id>https://mzrn.sh/2023/03/03/never-include-user-input-text-in-welcome-emails</id><content type="html" xml:base="https://mzrn.sh/2023/03/03/never-include-user-input-text-in-welcome-emails/"><![CDATA[<p>This is one of those articles where reading the title is enough. No need to read the rest, just go and make sure you are not breaking this rule on any of your existing websites, and keep in mind to never break it in the future.</p>

<p>If you are not convinced yet, read on. But know that you are now wasting 5 precious minutes you can otherwise spend on making your welcome emails less vulnerable.</p>

<p>Before we move on, let me clarify that this rule applies to welcome emails sent after direct email sign-ups. Sign-ups via OAuth providers like Gmail, Twitter, GitHub, etc. are by default more secured against the vulnerability I am about to discuss.</p>

<p>Here’s what happened to me 👇</p>

<p>I run a micro SaaS called <a href="https://weightless.so" target="_blank">Weightless</a>. Funny enough, all this tool does is collect email addresses and send email messages, so if anyone’s taking their email security seriously, it should be me. And that’s what I thought I was doing.</p>

<p>However, all my security efforts so far have been around making sure none of my users abuse Weightless to send spam to their subscribers. That’s not where the attacker struck! Because to even attempt spamming someone with Weightless, they would need to become a paid user first, and then to slip through my world-class 👀 security filters. Not efficient! Instead, why not make <em>me</em> spam people on their behalf, and for free?</p>

<p>This became possible by me trying to be too helpful too early, even before confirming the email address used at sign-up belonged to the person who signed up.</p>

<p>This is how my typical welcome email used to look:</p>

<p><img src="/assets/uploads/frame-2.png" alt="Typical welcome email sent by Weightless" title="Typical welcome email sent by Weightless" /></p>

<p>Now let’s see how the same email turned out after my attacker decided to put some imagination to it:</p>

<p><img src="/assets/uploads/frame-1-1-.png" alt="Welcome email sent by Weightless when being abused" title="Welcome email sent by Weightless when being abused" /></p>

<p>That’s an actual email my SaaS sent to someone. And there were hundreds more, before <a href="Postmarkapp.com" target="_blank">Postmark</a>, mailer daemon bless them, paused outgoing emails on my account and reached out to me.</p>

<p>The attack is quite simple in retrospect: the attacker saw I was including some user-input text in the welcome emails, and started a slow brute-force attack, most likely through an automated bot that was going through the list of real email addresses, signing up everyone on it with some irregular time intervals (maybe it was even less sophisticated and I am giving someone some new ideas here!).</p>

<p>There must be countless spammers abusing countless websites with this technique right now. And your website may well be among them if you, like me, decided that your welcome email can contain anything other the the email confirmation link. Or if, for example, you are passing on unconfirmed emails, together with other user-input data like name, company, etc. to your CRM software, or to the hands of your marketing team.</p>

<p>Consider this simple scenario:</p>

<p>You have a sign-up form with name and email inputs on it. Now pair it with a welcome email like this:</p>

<blockquote>
  <p><em>Hey NAME, thanks for signing up. Please click here to confirm your email.</em></p>
</blockquote>

<p>So if <strong><em>Joe</em></strong> with the email <strong><em>joe@example.com</em></strong> signs up, they will get this welcome message:</p>

<blockquote>
  <p><em>Hey Joe, thanks for signing up. Please click here to confirm your email.</em></p>
</blockquote>

<p>Nothing wrong, right? Wrong!</p>

<p>Here’s what that email looks like when someone is after Joe’s money:</p>

<blockquote>
  <p><em>Hey Joe. Here’s your $99 welcome gift! http://example.com/scam, thanks for signing up. Please click here to confirm your email.</em></p>
</blockquote>

<p>To do this, the attacker only needs to enter <strong><em>Joe. Here’s your $99 welcome gift! http://example.com/scam</em></strong> in your name field. You are doing all the rest!</p>

<p>Pure evil!</p>

<p>If you look at my email screenshots again, you will see that, if anything, my attacker was not creative enough, and I believe they could have done much better. I used to display the same user-input text, mailing list name, twice: once in the subject line, and then in the body. And in both instances, the way they phrased it, resulted in an incoherent message:</p>

<blockquote>
  <p><em>Welcome to Weightless.so 🪶  Here’s your setup guide for ‘💳 BAM $1966: http://example.com/scam 💳’</em></p>

  <p><em>Your mailing list 💳 BAM $1966: http://example.com/scam 💳 is ready.</em></p>
</blockquote>

<p>Here’s what my subject line would look like if they put <strong><em>$1966’ welcome gift: http://example.com/scam ‘get it now</em></strong> as the mailing list name:</p>

<blockquote>
  <p><em>Welcome to Weightless.so 🪶  Here’s your setup guide for ‘$1966’ welcome gift: http://example.com/scam ‘get it now’</em></p>
</blockquote>

<p>And here’s how their scam link would appear in the body, had they entered <strong><em>created has been, and our $1966 signup prize won you have, &lt;(°.°)&gt; and at http://example.com/scam it</em></strong> as the mailing list name:</p>

<blockquote>
  <p><em>Your mailing list created has been, and our $1966 signup prize won you have, &lt;(°.°)&gt; and at http://example.com/scam it is ready.</em></p>
</blockquote>

<p>You see? A little more creativity and I would suddenly be helping someone run a Yoda-themed scam, and it’s not even the baby Yoda! Luckily enough, it’s not too hard to prevent something like that from happening:</p>

<p><strong>Never include any user-input text in welcome emails, or any other type of emails triggered by submitting publicly accessible forms, where the receiver’s email address is part of the submitted data.</strong></p>]]></content><author><name>mzrnsh</name></author><category term="email" /><category term="security" /><category term="saas" /><summary type="html"><![CDATA[This is one of those articles where reading the title is enough. No need to read the rest, just go and make sure you are not breaking this rule on any of your existing websites, and keep in mind to never break it in the future.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mzrn.sh/assets/uploads/dontdothis.png" /><media:content medium="image" url="https://mzrn.sh/assets/uploads/dontdothis.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Using Airtable as a Jekyll website database</title><link href="https://mzrn.sh/2022/04/29/using-airtable-as-a-jekyll-website-database/" rel="alternate" type="text/html" title="Using Airtable as a Jekyll website database" /><published>2022-04-29T04:53:23+00:00</published><updated>2022-04-29T04:53:23+00:00</updated><id>https://mzrn.sh/2022/04/29/using-airtable-as-a-jekyll-website-database</id><content type="html" xml:base="https://mzrn.sh/2022/04/29/using-airtable-as-a-jekyll-website-database/"><![CDATA[<p>Can you use Airtable as the database for a Jekyll website? <strong>Yes</strong>, but it may
not work as you expect:</p>

<p>At the end of the day, all Jekyll does is generate a bunch of static HTML files.
Anything non-static happens at build step. So yes, you can access your Airtable
data, but not in real time. Each time you update your Airtable base, the website
will need to be rebuilt to reflect those changes.</p>

<p>Okay, but <em>why</em> use a dynamic database for a static site? Reasons may vary. For
most projects, you probably won’t need this but sometimes it makes sense. For
example, things might become cumbersome if your website grows too data-heavy for
a static site, or if you need to collaborate with non-developers. This is where
Airtable comes in: instead of manually updating lengthy data files, you can work
and collaborate on your data in Airtable.</p>

<p>Since this method makes use of the Jekyll-native <code class="language-plaintext highlighter-rouge">_data</code> folder approach, most
existing Jekyll projects should be able to use it without any significant
changes. If you stick with the attribute names present in your <code class="language-plaintext highlighter-rouge">_data</code> files and
reuse them in Airtable, you shouldn’t even need to touch <em>any</em> of your template
files.</p>

<p>I love when tutorials go with real-world examples instead of FooBar, to-do
list, and now Wordl clone projects, so we will be creating a real website called
UpToDate, which will keep us up to date with the latest releases of the
libraries, frameworks or programming languages we care about.</p>

<hr />

<p><em><strong>UPDATE</strong>: UpToDate may have started as a pet project that helped me write
this article but it’s grown into a real website now, that developers like you
rely on to keep themselves updated. It’s even got a its own domain name:
<a href="https://up-to.date" target="_blank">up-to.date</a>!</em></p>

<hr />

<p>Okay, Let’s go!</p>

<h2 id="1-create-a-new-jekyll-project">1. Create a new Jekyll project</h2>

<p>If you are new to Jekyll or need a refresher, check out my in-depth guide on
<a href="https://mzrn.sh/2022/04/09/starting-a-blank-jekyll-site-with-tailwind-css-in-2022/">starting a new Jekyll site</a>.
Otherwise, these quick steps should suffice.</p>

<p>Initiate a new Jekyll project from you terminal:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jekyll new uptodate
</code></pre></div></div>

<p>CD to your project and bundle the gems:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>uptodate
bundle
</code></pre></div></div>

<p>Let’s now update the default config. Open the <code class="language-plaintext highlighter-rouge">_config.yml</code> file and change its content to your liking. For UpToDate, it will look like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Update default values</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">UpToDate</span>
<span class="na">email</span><span class="pi">:</span> <span class="s">keep-me@up-to.date</span>
<span class="na">description</span><span class="pi">:</span> <span class="pi">&gt;-</span>
  <span class="s">Keep track of the latest releases of your favorite</span>
  <span class="s">libraries, frameworks and programming languages</span>
<span class="na">baseurl</span><span class="pi">:</span> <span class="s1">'</span><span class="s">'</span>
<span class="na">url</span><span class="pi">:</span> <span class="s">https://up-to.date</span>

<span class="c1"># Put my own twitter and github usernames</span>
<span class="na">twitter_username</span><span class="pi">:</span> <span class="s">mzrnsh</span>
<span class="na">github_username</span><span class="pi">:</span>  <span class="s">mzrnsh</span>

<span class="c1"># Keep the default theme and plugins</span>
<span class="na">theme</span><span class="pi">:</span> <span class="s">minima</span>
<span class="na">plugins</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">jekyll-feed</span>
</code></pre></div></div>

<p>Let’s confirm there are no issues so far:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle exec jekyll serve
</code></pre></div></div>

<p>Go to <a href="http://localhost:4000">http://localhost:4000</a>. If you see something like this, you’re ready for the next step:</p>

<p><img src="/assets/uploads/default-jekyll-homepage.png" alt="Jekyll ready" title="Jekyll ready" /></p>

<h2 id="2-display-some-data-the-native-way">2. Display some data the native way</h2>

<p>Before we throw in Airtable, let’s add some data the native way and display it
on a page to make sure it all worked before we started breaking things.</p>

<p>Add <code class="language-plaintext highlighter-rouge">_data</code> folder to the project’s root, and create a YAML file in it. In our
case, it will be <code class="language-plaintext highlighter-rouge">_data/things.yml</code> file as we will be tracking the version
numbers of all sorts of things. Let’s add a couple entries to it:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Ruby on Rails</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">7.0.2.3</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Tailwind CSS</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">3.0.24</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Font Awesome</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">6.1.0</span>
</code></pre></div></div>

<p>Next let’s display this data on the homepage. Open the <code class="language-plaintext highlighter-rouge">index.markdown</code> file and change its content to something like this:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">home</span>
<span class="nn">---</span>

{% for thing in site.data.things %}
<span class="p">  -</span> {{ thing.name }}: {{ thing.version }}
{% endfor %}
</code></pre></div></div>

<p>Since I am using the default Minima theme, the homepage comes with a blog section. Let’s get rid of it. The proper way to do this is overriding the default layout, or using a new layout but that is beyond the scope of this tutorial. Instead, let’s just delete the demo blog post file from the <code class="language-plaintext highlighter-rouge">_posts</code> directory. With no posts left, the theme will hide the blog section and the homepage will look nice and clean:</p>

<p><img src="/assets/uploads/uptodate-home-clean.png" alt="UpToDate - clean homepage" title="UpToDate - clean homepage" /></p>

<h2 id="3-create-an-airtable-base">3. Create an Airtable base</h2>

<p>With the initial version of UpToDate looking and working as intended, we can now
start working on our Airtable integration.</p>

<p>First we need an Airtable base that houses the same data as we are displaying on
our homepage. Head over to Airtable dashboard and create a new base. Here’s what
it looks like for UpToDate:</p>

<p><img src="/assets/uploads/uptodate-base.png" alt="UpToDate Airtable base" title="UpToDate Airtable base" /></p>

<p>Mind the capitalization of the table and column names. We will need to refer
some of those values as strings in later steps.</p>

<p>I created this base manually as I have only 3 entries. If you have a lot more
data, Airtable has various import tools to make things easier for you.</p>

<div class="
  my-12 p-6 bg-gradient-to-br from-gray-700 to-gray-900 text-gray-50 shadow-md text-center -mx-8
  sm:rounded-lg md:-mx-12
">
  <div class="flex items-center gap-4">
    <div class="-rotate-12">
      <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
        <path stroke-linecap="round" stroke-linejoin="round" d="
            M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0
            100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234
            9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z
          " />
      </svg>
    </div>

    <div class="">
      <p class="m-0 text-sm">
        Pssst! Blogging on a static site? Collecting visitor emails has just gotten
        easier than ever with:
      </p>

      <a href="https://weightless.so" class="no-underline text-xl block my-6 text-indigo-400" target="_blank" data-umami-event="Click Weightless ad">
        👉
        <span class="underline">weightless.so</span>
        🪶
      </a>

      <p class="m-0 text-sm">
        Setting it up takes ~90 seconds and $0 USD
      </p>
    </div>

    <div class="rotate-12 scale-x-[-1]">
      <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
        <path stroke-linecap="round" stroke-linejoin="round" d="
            M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0
            100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234
            9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z
          " />
      </svg>
    </div>
  </div>

  <p class="mb-0 mt-6 text-xs text-gray-300">
    This is not a paid ad, it's a free one: Weightless is built by me 👋
  </p>
</div>

<h2 id="4-add-airtable-api-credentials">4. Add Airtable API credentials</h2>

<p>To speak with the Airtable API, we will need to use an API key and the base ID. Ideally, we don’t want to make them a part of the source code. Instead, we want to use environment variables. Let’s use the <code class="language-plaintext highlighter-rouge">jekyll-dotenv</code> gem for this:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"gem 'jekyll-dotenv'"</span> <span class="o">&gt;&gt;</span> Gemfile
bundle
</code></pre></div></div>

<p>If using git, make sure to include <code class="language-plaintext highlighter-rouge">.env</code> file in your <code class="language-plaintext highlighter-rouge">.gitignore</code>:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">".env"</span> <span class="o">&gt;&gt;</span> .gitignore
</code></pre></div></div>

<p>Now create the <code class="language-plaintext highlighter-rouge">.env</code> file in the root directory with the following variables:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AIRTABLE_API_KEY='YOUR_API_KEY'
AIRTABLE_BASE='YOUR_AIRTABLE_BASE_ID'
AIRTABLE_TABLE='YOUR_AIRTABLE_TABLE_NAME'
</code></pre></div></div>

<p>Replace the sample values <code class="language-plaintext highlighter-rouge">YOUR_API_KEY</code>, <code class="language-plaintext highlighter-rouge">YOUR_AIRTABLE_BASE_ID</code>, and <code class="language-plaintext highlighter-rouge">YOUR_AIRTABLE_TABLE_NAME</code>
with the actual values from your Airtable.</p>

<p>You can generate the API key on your <a href="https://airtable.com/account" target="_blank">Airtable account page</a>.</p>

<p>And you can grab the Airtable base ID from the URL in your browser address bar.
Open the desired base and copy the part of the URL right after <code class="language-plaintext highlighter-rouge">airtable.com/</code>:</p>

<p><img src="/assets/uploads/airtable-base-id.png" alt="Airtable base ID" title="Airtable base ID" /></p>

<p>As for Airtable table name, it’s the name you gave to your table. For UpToDate
it is “things” string:</p>

<p><img src="/assets/uploads/airtable-table-name.png" alt="Airtable table name" title="Airtable table name" /></p>

<p><strong>Important:</strong> Airtable API keys are unscoped, meaning they have the same
permissions as your user account, across all the bases you have access to. If
this concerns you, the ‘official’ way around it is to create a new account with
limited permissions and generate an API key from that account.</p>

<h2 id="5-create-a-custom-jekyll-plugin-for-airtable">5. Create a custom Jekyll plugin for Airtable</h2>

<p>First let’s install the <code class="language-plaintext highlighter-rouge">airtable</code> and <code class="language-plaintext highlighter-rouge">activesupport</code> gems that we will need
for our plugin:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"gem 'airtable'"</span> <span class="o">&gt;&gt;</span> Gemfile
<span class="nb">echo</span> <span class="s2">"gem 'activesupport'"</span> <span class="o">&gt;&gt;</span> Gemfile
bundle
</code></pre></div></div>

<p>Next, we will be casually creating a custom Jekyll plugin! If you’ve already done this, you know there’s nothing scary about it. If not, see for yourself:</p>

<p>Create <code class="language-plaintext highlighter-rouge">_plugins</code> directory in the project root and a file named <code class="language-plaintext highlighter-rouge">airtable.rb</code> in it. That’s it, technically speaking, we already have a custom plugin. Now let’s make it do something.</p>

<p>Open the freshly created <code class="language-plaintext highlighter-rouge">_plugins/airtable.rb</code> file in the editor and paste
the following:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'dotenv/load'</span>
<span class="nb">require</span> <span class="s1">'airtable'</span>
<span class="nb">require</span> <span class="s1">'active_support/all'</span>

<span class="n">airtable</span> <span class="o">=</span> <span class="no">Airtable</span><span class="o">::</span><span class="no">Client</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">ENV</span><span class="p">[</span><span class="s1">'AIRTABLE_API_KEY'</span><span class="p">])</span>
<span class="n">table</span> <span class="o">=</span> <span class="n">airtable</span><span class="p">.</span><span class="nf">table</span><span class="p">(</span><span class="no">ENV</span><span class="p">[</span><span class="s1">'AIRTABLE_BASE'</span><span class="p">],</span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'AIRTABLE_TABLE'</span><span class="p">])</span>

<span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="s2">"_data/</span><span class="si">#{</span><span class="no">ENV</span><span class="p">[</span><span class="s1">'AIRTABLE_TABLE'</span><span class="p">]</span><span class="si">}</span><span class="s2">.yml"</span><span class="p">,</span> <span class="s1">'w'</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span>
  <span class="n">data</span> <span class="o">=</span> <span class="n">table</span><span class="p">.</span><span class="nf">records</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:attributes</span><span class="p">)</span>
  <span class="n">warning</span> <span class="o">=</span> <span class="s2">"# Do not edit this file manually </span><span class="se">\n</span><span class="s2">"</span>

  <span class="n">file</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">warning</span><span class="p">,</span> <span class="n">data</span><span class="p">.</span><span class="nf">to_yaml</span><span class="p">)</span>
<span class="k">end</span>

</code></pre></div></div>

<p>Let’s read the code above line-by-line to make sure everything is clear:</p>

<ul>
  <li><strong>Lines 1-3:</strong> import the libraries we will need</li>
  <li><strong>Lines 5-6:</strong> connect to Airtable and retrieve the records from the table
that houses our data. As you see, this is where we use the environment
variables we set up earlier</li>
  <li><strong>Line 8:</strong> open the existing data file. As both the data file name and the
Airtable table name values are the same (in my case, “things”), I am using
<code class="language-plaintext highlighter-rouge">"_data/#{ENV['AIRTABLE_TABLE_NAME']}.yml"</code> as file path. If for some reason
those two values do not match in your project, you can simply specify the file
path as a string: <code class="language-plaintext highlighter-rouge">'_data/other_things.yml'</code></li>
  <li><strong>Line 9:</strong> read the table records retrieved from Airtable and set the <code class="language-plaintext highlighter-rouge">data</code>
variable’s value to it</li>
  <li><strong>Line 10:</strong> Add a reminder for our future selves that this is an
auto-generated file and shouldn’t be edited manually. Note that both the <code class="language-plaintext highlighter-rouge">#</code>
at the beginning and the <code class="language-plaintext highlighter-rouge">\n</code> at the end are required to keep the YAML file
valid</li>
  <li><strong>Line 12:</strong> finally, complete the plugin’s work by uptading the file contents</li>
</ul>

<p>Okay, let’s now confirm our plugin works. Start the Jekyll server, or if it’s
already running, restart it:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>jekyll serve
</code></pre></div></div>

<p>Did the command run without errors? Nice!</p>

<p>Now open your data file and check if it was updated. From something like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Ruby on Rails</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">7.0.2.3</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Tailwind CSS</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">3.0.24</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Font Awesome</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">6.1.0</span>

</code></pre></div></div>

<p>It should have changed to something like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Do not edit this file manually</span>
<span class="nn">---</span>
<span class="pi">-</span> <span class="kt">!ruby/hash:ActiveSupport::HashWithIndifferentAccess</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">3.0.24</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">Tailwind CSS</span>
  <span class="na">id</span><span class="pi">:</span> <span class="s">rec41IcxHOE3f9KU1</span>
<span class="pi">-</span> <span class="kt">!ruby/hash:ActiveSupport::HashWithIndifferentAccess</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">7.0.2.3</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">Ruby on Rails</span>
  <span class="na">id</span><span class="pi">:</span> <span class="s">recXh3RJKAfcsM4Kx</span>
<span class="pi">-</span> <span class="kt">!ruby/hash:ActiveSupport::HashWithIndifferentAccess</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">6.1.0</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">Font Awesome</span>
  <span class="na">id</span><span class="pi">:</span> <span class="s">recpx0BkIPC8VWkD9</span>

</code></pre></div></div>

<p>Yes? Great! 🎉 Your Jekyll site is now connected to Airtable and each time you
build or serve it, the fresh data will be fetched from Airtable.</p>

<h2 id="6-automate-rebuilds">6. Automate rebuilds</h2>

<p>Hold on, we’re not quite done yet! We likely want changes in Airtable to reflect
on the website automatically, without manual rebuilds and redeploys.</p>

<p>There are multiple ways to achieve this, and the best option might depend on
your Airtable plan, production hosting and personal tastes. At the time of me
writing this, Airtable only supports webhooks on their enterprise plan, so
unless you have that, the outline of your automation will probably look a little
something like this:</p>

<ul>
  <li>Your hosting platform allows redeploys by URL pinging</li>
  <li>A ‘bot’ regularly checks your Airtable base for changes</li>
  <li>When the bot detects changes, it pings the redeploy URL</li>
</ul>

<p>But wait, this way the website will be redeployed from the same commit as
before, so there will be no changes, right? Wrong! When the website is re-built,
the plugin we just wrote will make a fresh request to Airtable during the build
step, fetching the updated data, resulting the contents of your <code class="language-plaintext highlighter-rouge">_data</code> folder
on production environment to differ from what you have in your latest commit.</p>

<p>Really good, it should all work fine, in theory. But let’s go with an example to
make sure it works for real. To host UpToDate I am using Netlify, and here’s how
I automated rebuilds there 👇</p>

<h3 id="example-auto-deploy-on-netlify-via-pipedream"><strong>Example: Auto-deploy on Netlify via Pipedream</strong></h3>

<p><a href="https://pipedream.com/" target="_blank">Pipedream</a> is a low code automation
platform with a generous free plan and some powerful features, and it’s perfect
for our use case.</p>

<p>The next steps assume that you have a working Netlify deployment and a Pipedream
account.</p>

<h4 id="a-generate-a-netlify-build-hook"><strong>a. Generate a Netlify build hook</strong></h4>
<p>Let’s start by creating a deploy URL in Netlify. They call such URLs “Build
hooks” and they’re created from your project’s “Build and deploy” settings:</p>

<p><img src="/assets/uploads/netlify-build-hooks-airtable.png" alt="Netlify build hooks Airtable" title="Netlify build hooks Airtable" />
<span>https://app.netlify.com/sites/YOUR_PROJECT/settings/deploys#build-hooks</span></p>

<p>It’s a good idea to give build hooks descriptive names to remind your future
self what it is used for, and so what is likely to break if you delete it.</p>

<h4 id="b-connect-airtable-to-pipedream"><strong>b. Connect Airtable to Pipedream</strong></h4>
<p>Let’s now head to Pipedream and open the “Accounts” menu. Click “Connect an app”
button and select “Airtable” in the options. Then follow the steps in the wizard
to complete the connection.</p>

<h4 id="c-create-a-pipedream-source"><strong>c. Create a Pipedream source</strong></h4>
<p>With Airtable connected to our Pipedream, let’s add a new source that will make
use of this connection. This will be the bot I mentioned above, tirelessly
checking our data, hoping to detect any changes in it.</p>

<ul>
  <li>Go to the “sources” menu</li>
  <li>Click “New +” button</li>
  <li>Pick “Airtable” as the app and “New or Modified Records” as the action</li>
</ul>

<p>Now you need to configure your source:</p>
<ul>
  <li>Choose values for the “Airtable account”, “Base” and “Table” dropdowns</li>
  <li>Set up the timer. I went with “Every 24 hours” but you can do it more
frequently if you like. Just make sure you are staying within your Pipedream
plan allowance</li>
  <li>Name the source. I went with “UpToDate changes”</li>
</ul>

<p>Here’s what my source config screen looks like:</p>

<p><img src="/assets/uploads/pipedream-new-source.png" alt="Pipedream new source" title="Pipedream new source" /></p>

<p>Once you create the source you need to make a couple changes, otherwise the
out-of-box version of the source will emit an event for each change. We don’t
want that as it will cause multiple Netlify deploys in case there are multiple
changes since the last check.</p>

<p>Open the source and go to the “Configuration” tab. Here, in the code editor
locate the guard clause that returns from the function in case there are no
changes. Right now it’s this piece of code on line 45 (this may change):</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">data</span><span class="p">.</span><span class="nx">records</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">No new or modified records.</span><span class="dl">"</span><span class="p">);</span>
  <span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We don’t want to touch this part code, or anything that comes before that. Only
the parts after it, up until the following lines close to the very end:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// We keep track of the timestamp of the current invocation</span>
<span class="k">this</span><span class="p">.</span><span class="nf">updateLastTimestamp</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
</code></pre></div></div>

<p>I replaced everything between those two pieces of code with this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ...</span>

<span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">data</span><span class="p">.</span><span class="nx">records</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">No new or modified records.</span><span class="dl">"</span><span class="p">);</span>
  <span class="k">return</span><span class="p">;</span>
<span class="p">}</span>

<span class="cm">/* 👇 My changes 👇 */</span>

<span class="k">this</span><span class="p">.</span><span class="nf">$emit</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="p">{</span>
  <span class="na">summary</span><span class="p">:</span> <span class="s2">`Changes detected: </span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">records</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2">`</span>
<span class="p">});</span>

<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`Emitted changes.`</span><span class="p">);</span>

<span class="cm">/* 👆 My changes 👆 */</span>

<span class="c1">// We keep track of the timestamp of the current invocation</span>
<span class="k">this</span><span class="p">.</span><span class="nf">updateLastTimestamp</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>

<span class="c1">// ...</span>
</code></pre></div></div>

<h4 id="d-create-a-pipedream-workflow"><strong>d. Create a Pipedream workflow</strong></h4>

<p>Go to “Workflows” menu and click the “New +” button. It will pull up a menu to
select a trigger, where you want to click the “USE ONE OF YOUR EXISTING SOURCES”
button. This will let you select the source we created on the previous step.</p>

<p>Then add a step to this workflow. Pick “Send any HTTP Request” option. In the
request configuration pick POST request type (that’s what Netlify build hooks
expect) and paste the build hook URL we generated on step <em>a</em>.</p>

<p>Now give this workflow a good name it deserves. I went with “Rebuild
UpToDate”. And click “Deploy”. Did you? Then you’re DONE! 🤩</p>

<p>From now on, when you, or others, update the data in Airtable, Pipedream will
catch the changes, and trigger a Netlify deploy. And Netlify will run our plugin
that will update Jekyll’s <code class="language-plaintext highlighter-rouge">_data</code> folder to reflect those changes, meaning the
the website will get updated without any extra effort from us 🙀</p>]]></content><author><name>mzrnsh</name></author><category term="jekyll," /><category term="airtable," /><category term="jamstack," /><category term="static" /><category term="website" /><summary type="html"><![CDATA[Can you use Airtable as the database for a Jekyll website? Yes, but it may not work as you expect:]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mzrn.sh/assets/uploads/jekyll-airtable.png" /><media:content medium="image" url="https://mzrn.sh/assets/uploads/jekyll-airtable.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Starting a blank Jekyll site with Tailwind CSS in 2022</title><link href="https://mzrn.sh/2022/04/09/starting-a-blank-jekyll-site-with-tailwind-css-in-2022/" rel="alternate" type="text/html" title="Starting a blank Jekyll site with Tailwind CSS in 2022" /><published>2022-04-09T14:12:51+00:00</published><updated>2022-04-09T14:12:51+00:00</updated><id>https://mzrn.sh/2022/04/09/starting-a-blank-jekyll-site-with-tailwind-css-in-2022</id><content type="html" xml:base="https://mzrn.sh/2022/04/09/starting-a-blank-jekyll-site-with-tailwind-css-in-2022/"><![CDATA[<p>Most websites I build start off as a blank Jekyll site with Tailwind CSS on top.</p>

<p>It’s definitely no rocket science, but every now and then things don’t go so
smooth: a new device, something out of date, something I forgot.. So I
decided to document the process as a detailed guide to help me, or someone
else, when that happens.</p>

<p>I tried to keep it beginner-friendly, so if you are a seasoned developer, don’t
be surprised if you see me telling you things like how to add a line to a file
from terminal, or that you should be using git.</p>

<hr />

<p><em><strong>UPDATE</strong>: I made a <a href="https://github.com/mzrnsh/jekyllwind" target="_blank">Jekyll + Tailwind boilerplate</a> based on this guide. It will save you some time with fresh projects. Adding Tailwind CSS to an existing Jekyll site? Read on ⤵</em></p>

<hr />

<h2 id="0-development-environment">0. Development environment:</h2>

<p>If this is the first time you are building a Jekyll website on your machine, you
might want to check out the official Jekyll
<a href="https://jekyllrb.com/docs/installation/#requirements" target="_blank">documentation</a>
regarding the prerequisites.</p>

<p>Here’s my current development environment:</p>

<ul>
  <li>macOS 12.2.1</li>
  <li>Ruby 3.1.1</li>
  <li>Bundler 2.3.7</li>
  <li>Jekyll 4.2.2</li>
  <li>Node.js 16.14.2</li>
</ul>

<p>The differences with the minor  [<code class="language-plaintext highlighter-rouge">x.x.X</code>] and patch [<code class="language-plaintext highlighter-rouge">x.X.x</code>] version numbers shouldn’t matter but with the major [<code class="language-plaintext highlighter-rouge">X.x.x</code>] numbers might.</p>

<h2 id="1-start-a-new-jekyll-project">1. Start a new Jekyll project</h2>

<p>Let’s create a little static website called <strong>UpToDate</strong>. Run this in your
terminal:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jekyll new uptodate <span class="nt">--blank</span>
<span class="nb">cd </span>uptodate
</code></pre></div></div>

<h2 id="2-set-up-git">2. Set up Git:</h2>

<p>In your terminal, run:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git init
</code></pre></div></div>

<p>In the root directory of your project create a file named <code class="language-plaintext highlighter-rouge">.gitignore</code> and paste
the following in it:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_site/
.sass-cache/
.jekyll-cache/
.jekyll-metadata
node_modules

</code></pre></div></div>

<h2 id="3-install-ruby-gems">3. Install Ruby gems</h2>

<p>The <code class="language-plaintext highlighter-rouge">--blank</code> flag makes things really blank, so the <code class="language-plaintext highlighter-rouge">Gemfile</code> needs to be
created manually. Add a file named <code class="language-plaintext highlighter-rouge">Gemfile</code> to the root directory with the
following content:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">source</span> <span class="s1">'https://rubygems.org'</span>

<span class="n">gem</span> <span class="s1">'jekyll'</span>
<span class="n">gem</span> <span class="s1">'webrick'</span>

</code></pre></div></div>

<p>Now let Bundler install those gems. In your terminal, run the install command:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle
</code></pre></div></div>

<p>If you see a message starting with “<em>Bundle complete!</em>”, it means everything went according to
the plan. Let’s confirm this by firing up the Jekyll server from terminal:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>jekyll serve
</code></pre></div></div>

<p>Open <a href="http://localhost:4000" target="_blank">localhost:4000</a> in your browser.
You should see something like this:</p>

<p><img src="/assets/uploads/jekyll-ready.png" alt="You can proceed!" title="Jekyll ready" /></p>

<h2 id="4-add-postcss">4. Add PostCSS</h2>

<p>In order to use Tailwind CSS, we first need to install and configure PostCSS.</p>

<p>Let’s install the PostCSS gem for Jekyll. Run the following in your terminal:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"gem 'jekyll-postcss'"</span> <span class="o">&gt;&gt;</span> Gemfile
bundle
</code></pre></div></div>

<p>For this plugin to work properly, some changes need to be made in Jekyll
configuration. Open the <code class="language-plaintext highlighter-rouge">_config.yml</code> file and add the following lines at the end:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">plugins</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">jekyll-postcss</span>

<span class="na">postcss</span><span class="pi">:</span>
  <span class="na">cache</span><span class="pi">:</span> <span class="kc">false</span>

</code></pre></div></div>

<p>Disabling cache is needed for Tailwind CSS’s JIT engine. Without this,
the server would need to restart after each change.</p>

<p>Now create a <code class="language-plaintext highlighter-rouge">postcss.config.js</code> file in the root directory and paste the
following in it:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span>
    <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">tailwindcss</span><span class="dl">'</span><span class="p">),</span>
    <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">autoprefixer</span><span class="dl">'</span><span class="p">),</span>
    <span class="p">...(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">JEKYLL_ENV</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">production</span><span class="dl">'</span>
      <span class="p">?</span> <span class="p">[</span><span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">cssnano</span><span class="dl">'</span><span class="p">)({</span> <span class="na">preset</span><span class="p">:</span> <span class="dl">'</span><span class="s1">default</span><span class="dl">'</span> <span class="p">})]</span>
      <span class="p">:</span> <span class="p">[])</span>
  <span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Autoprefixer and cssnano packages are optional, but they are recommended for
production builds.</p>

<div class="
  my-12 p-6 bg-gradient-to-br from-gray-700 to-gray-900 text-gray-50 shadow-md text-center -mx-8
  sm:rounded-lg md:-mx-12
">
  <div class="flex items-center gap-4">
    <div class="-rotate-12">
      <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
        <path stroke-linecap="round" stroke-linejoin="round" d="
            M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0
            100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234
            9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z
          " />
      </svg>
    </div>

    <div class="">
      <p class="m-0 text-sm">
        Pssst! Blogging on a static site? Collecting visitor emails has just gotten
        easier than ever with:
      </p>

      <a href="https://weightless.so" class="no-underline text-xl block my-6 text-indigo-400" target="_blank" data-umami-event="Click Weightless ad">
        👉
        <span class="underline">weightless.so</span>
        🪶
      </a>

      <p class="m-0 text-sm">
        Setting it up takes ~90 seconds and $0 USD
      </p>
    </div>

    <div class="rotate-12 scale-x-[-1]">
      <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
        <path stroke-linecap="round" stroke-linejoin="round" d="
            M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0
            100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234
            9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z
          " />
      </svg>
    </div>
  </div>

  <p class="mb-0 mt-6 text-xs text-gray-300">
    This is not a paid ad, it's a free one: Weightless is built by me 👋
  </p>
</div>

<p>Now let’s install those packages. I use Yarn (NPM is fine as well):</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add postcss@latest tailwindcss@latest autoprefixer@latest cssnano@latest <span class="nt">-D</span>
</code></pre></div></div>

<h2 id="5-add-tailwind-css-finally">5. Add Tailwind CSS, finally</h2>

<p>First create a <code class="language-plaintext highlighter-rouge">tailwind.config.js</code> file in the root directory with the following contents:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">content</span><span class="p">:</span> <span class="p">[</span>
    <span class="dl">'</span><span class="s1">./_drafts/**/*.html</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">./_includes/**/*.html</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">./_layouts/**/*.html</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">./_posts/*.md</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">./*.md</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">./*.html</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">],</span>
  <span class="na">theme</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">theme</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">extend</span><span class="p">:</span> <span class="p">{},</span>
    <span class="p">},</span>
  <span class="p">},</span>
  <span class="na">plugins</span><span class="p">:</span> <span class="p">[]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The above config lets Tailwind know where its utility classes might be located. If you add new directories for your posts, pages, or partials, you will need to update the <code class="language-plaintext highlighter-rouge">content</code> array accordingly.</p>

<p>By default, Jekyll works with SASS so we need to make a couple more changes.</p>

<p>Locate the <code class="language-plaintext highlighter-rouge">assets/css/main.scss</code> file and change its extension from <code class="language-plaintext highlighter-rouge">.scss</code> to <code class="language-plaintext highlighter-rouge">.css</code>.
Unless you’re planning to use SASS, you might want to delete the <code class="language-plaintext highlighter-rouge">_sass</code> directory.</p>

<p>Now open <code class="language-plaintext highlighter-rouge">assets/css/main.css</code> and change its content to the following:</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">---</span>
<span class="nt">---</span>

<span class="k">@tailwind</span> <span class="n">base</span><span class="p">;</span>
<span class="k">@tailwind</span> <span class="n">components</span><span class="p">;</span>
<span class="k">@tailwind</span> <span class="n">utilities</span><span class="p">;</span>
</code></pre></div></div>

<p>Note those hyphens at the beginning. That’s YAML front matter and our main css file <em>must</em> start with it.</p>

<p>Once again, let’s confirm everything’s working as intended. Start the Jekyll server, or if it’s already running, restart it:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>jekyll serve <span class="nt">--livereload</span>
</code></pre></div></div>

<p>Open <a href="http://localhost:4000" target="_blank">localhost:4000</a> in your browser.</p>

<p>Things should look a little bit different from what we last saw, and a keen eye
might even recognize Tailwind Preflight at play, stripping the browser default
styles, but of course we need to make things red to be super sure.</p>

<p>Open <code class="language-plaintext highlighter-rouge">_layouts/default.html</code> and add <code class="language-plaintext highlighter-rouge">class="text-red-500"</code> attribute to the
<code class="language-plaintext highlighter-rouge">&lt;body&gt;</code> tag. Now switch back to your browser. Since we used the <code class="language-plaintext highlighter-rouge">--livereload</code>
flag, you should be seeing this already:</p>

<p><img src="/assets/uploads/tailwind-ready.png" alt="You’re all done!" title="Tailwind ready" /></p>

<p>YES? Congrats! You are ready to use Tailwind CSS in your project 🥳</p>

<h2 id="6-bonus-content-production-deployment">6. Bonus content: production deployment</h2>

<h3 id="61-netlify">6.1. Netlify</h3>

<p>Netlify is my favorite tool when it comes to hosting static sites. For some
reason, their default settings for deploying Jekyll sites seem to not play that
well with my guide above.</p>

<p>If you experience any issues related to PostCSS during deploy, specifying
<code class="language-plaintext highlighter-rouge">JEKYLL_ENV</code> in the build command might help. Instead of the Netlify-provided
<code class="language-plaintext highlighter-rouge">bundle exec jekyll build</code>, use <code class="language-plaintext highlighter-rouge">JEKYLL_ENV=production bundle exec jekyll build</code>.</p>

<p>My build settings look like this, and it works:</p>

<p><img src="/assets/uploads/netlify-build-settings-jekyll.png" alt="Netlify build settings for Jekyll" title="Netlify build settings for Jekyll" /></p>

<h3 id="62-github-pages">6.2. GitHub Pages</h3>

<p>If you set up your Tailwind CSS + Jekyll website according to this guide and you
now wish to host it on GitHub Pages, it won’t work out of box. Long story short,
you need to do it via GitHub Actions.</p>

<p>Luckily, I <a href="https://mzrn.sh/2023/10/26/how-to-use-tailwind-css-with-jekyll-on-github-pages/">wrote</a> about that as well.</p>]]></content><author><name>mzrnsh</name></author><category term="jekyll" /><category term="tailwind-css" /><summary type="html"><![CDATA[Most websites I build start off as a blank Jekyll site with Tailwind CSS on top.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mzrn.sh/assets/uploads/jekyll-tailwindcss.png" /><media:content medium="image" url="https://mzrn.sh/assets/uploads/jekyll-tailwindcss.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Freelance Ladder and how to climb it</title><link href="https://mzrn.sh/2021/11/27/the-freelance-ladder-and-how-to-climb-it/" rel="alternate" type="text/html" title="The Freelance Ladder and how to climb it" /><published>2021-11-27T18:23:06+00:00</published><updated>2021-11-27T18:23:06+00:00</updated><id>https://mzrn.sh/2021/11/27/the-freelance-ladder-and-how-to-climb-it</id><content type="html" xml:base="https://mzrn.sh/2021/11/27/the-freelance-ladder-and-how-to-climb-it/"><![CDATA[<p>Freelancing can be a highly rewarding career path. When I got started just over 5 years ago, I had spent as much time being an employee. Looking back, my freelance years have been more fulfilling, interesting, and profitable than my office years.</p>

<p>Wasn’t all roses though, especially in the beginning. There’s this thing I call the Freelance Ladder, and I had to climb it, just like most other freelancers I know. When I say <em>had to</em>, you might think there was no other option for me, and that’s true. But even if there was, I strongly believe that this is also a ladder you <em>want</em> and <em>need</em> to climb to achieve a long-term success. I’ve seen a few aspiring freelancers take shortcuts. Some, I helped do it. But this doesn’t usually end well.</p>

<p>Moving up the ladder is not only about earning more, but also about learning more. And you have a lot to learn and figure out: how to sell yourself, how not to sell yourself short, what kind of clients you like and dislike to work with, whether you should specialize or generalize. These things take time and practice. There’s no shortcut around them.</p>

<p>Here’s the Freelance Ladder, and how you climb it:</p>

<ol>
  <li><strong>Noob:</strong> sell output for low fixed price</li>
  <li><strong>Amateur:</strong> sell input for low hourly rate</li>
  <li><strong>Pro:</strong> sell input for high hourly rate</li>
  <li><strong>Legend:</strong> sell outcome for high fixed price</li>
</ol>

<p><img src="/assets/uploads/freelance-ladder.jpg" alt="A man ascending the steps" title="The Freelance Ladder" />
<span>Illustration by <a href="https://undraw.co" target="_blank">unDraw</a></span></p>

<p>A few notes before we dive in:</p>

<ul>
  <li>It should go without saying, that the absolute key to your success is you always doing everything within your power to deliver the value you promised. This article won’t give you any tips or tricks how to cheat your way up</li>
  <li>This is all based on my personal experience being a freelancer. I build software, so my path might be most relevant for professionals from the software development industry. But I believe that for the most part, it can also apply to aspiring freelancers from other industries</li>
  <li>I am not claiming this is the only or the best way to succeed as a freelancer. But this is the one I know and has served me well</li>
</ul>

<p>Let’s go.</p>

<h2 id="1-noob-sell-output-for-low-fixed-price">1. Noob: sell output for low fixed price</h2>

<p>You may already be an expert in your field, but until you have something to show for, you are a noob. Know it, and own it.</p>

<p>Get on those freelance platforms and start bidding. But don’t waste time chasing exciting and well-paying jobs. Those attract the freelancers you aren’t yet equipped to compete with. Instead focus on boring, low-paying gigs that require one or more of your specific skills.</p>

<p>The best jobs, both in terms of your chances to land it, and their usefulness towards your ascent on the ladder, are highly descriptive tasks with clear deliverables that leaves not much room for imagination and uncertainty. The client knows what they want and how much they are willing to pay for it. The input, or the effort you put in, doesn’t matter to them. They are paying for the output.</p>

<p>Let’s go with an example. Say I am a freelance WordPress developer on the noob level. This is what a perfect job would look like for me:</p>

<blockquote>
  <p><em>Update Plugin X from version 2.3.1 to version 4.7.5. I don’t want it to be updated to the latest 6.0.0 version as it’s not compatible with the Plugin Y version 1.0.6 that we need to keep using. Our previous developer changed its code and we can’t touch it.</em></p>

  <p><em>Budget: $100</em></p>
</blockquote>

<p>It’s perfect for a few reasons:</p>

<ul>
  <li>Not a reusable experience: chances of someone else having the exact same problem is close to 0</li>
  <li>Low fixed budget: it’s a safe bet that not a single non-noob freelancer will bother to bid on it. The upside has a hard limit, the downside does not</li>
  <li>The project is likely a mess: the previous developer could have left other easter eggs to make your life miserable. Even not every noob would bid on this, only the ones as desperate as you to land a job</li>
  <li>It’s specific enough to stand out: since most other applicants are noobs, no-one stands out by default. This is a perfect chance for someone with relevant skills and experience to shine</li>
</ul>

<p>As a noob, winning hourly jobs is very hard. But there’s one more reason why you don’t even want such jobs just yet. If you are bidding on platforms like Upwork or Fiverr and you land an hourly job by selling yourself too short, this will stick out in your work history and might devalue you in the eyes of your future clients, making your climb longer than necessary.</p>

<h2 id="2-amateur-sell-input-for-low-hourly-rate">2. Amateur: sell input for low hourly rate</h2>

<p>After winning and successfully completing a few noob jobs, you should have built up some reputation on your freelance platform. You are now ready to step up. You may be tempted to continue winning the noob jobs, as it has become easier for you, but that’s not how you move up.</p>

<p>You already proved your input equals to an output of a certain value. Time to capitalize on this. You now have a chance to win more appealing jobs that pay hourly and last longer. Landing such a job will make your freelance journey more stable, enjoyable and profitable.</p>

<p>Your competition has changed though: instead of noobs, you are now competing with other amateurs and pros. This means your first few jobs will likely be close to the lower end of your desired hourly rate range. That’s okay, it will increase over time. Once you have established your place as an amateur and have a decent, long-term job, you can shake things up a bit to accelerate this growth. There’s no risk in failing. Here are a few things you can try:</p>

<ol>
  <li>Try to get a parallel job that pays slightly better than the current one</li>
  <li>Ask for a raise from your current client. Make sure to have the power to walk away. Another job or an offer for one gives this power to you</li>
  <li>Get yourself listed on a other freelance platforms - diversify</li>
  <li>Start reaching out to potential clients directly, without a freelance platform as the middleman</li>
</ol>

<p>If you are not in the acceleration game, no worries. Keep climbing up calmly. But make sure you are climbing. A good way to tell is whether or not your hourly rate and overall work satisfaction is growing. It should be, slowly but steadily.</p>

<h2 id="3-pro-sell-input-for-high-hourly-rate">3. Pro: sell input for high hourly rate</h2>

<p>There’s no clear line between the amateur and pro levels. It’s more like you keep doing things as an amateur until you one day realize you have become a pro.</p>

<p>And there are ways to tell. Things will have changed:</p>

<ul>
  <li>You can earn more while working less</li>
  <li>Winning jobs has become easier, the clients come to you now</li>
  <li>You have become picky who you want to work with</li>
  <li>The work you do is different. You have either specialized or generalized, or both</li>
  <li>You have more potential work than you can handle. You now casually reject job offers you’d only dream of as an amateur</li>
  <li>You have hired other freelancers</li>
  <li>You shake your head in disapproval while reading this article and go write a better one yourself</li>
</ul>

<p>If you find yourself doing some or all of the above, congrats - you made it! 🥳 You are now a pro freelancer.</p>

<p>But the biggest change by far, is realizing that your input is no longer measured by your output. Rather, it’s now measured by the outcome.</p>

<p>Your actions now directly affect you client’s bottom line. Sure, this was true before as well, to an extent. That’s why you were getting paid in the first place. But it was not under your control and was limited. Heck! Remember when you were a noob? Most of the times, you weren’t even told <em>why</em> you had to do what you had to do. Fast forward to now, and I bet that often it’s you who decides what to work on and what to not. The clients only tell you what goals they want to achieve and you figure out the rest.</p>

<p>One more round of congratulations is in order, you deserve it. Keep going, you are doing great! And don’t forget to keep increasing your hourly rate.</p>

<h2 id="4-legend-sell-outcome-for-high-fixed-price">4. Legend: sell outcome for high fixed price</h2>

<p>Be on the pro level long enough, and you will inevitably notice that your hourly rate growth is slowing down.  That’s a sign you are closing in on the ceiling. The ceiling is imposed by the industry you work in and once  you hit it, your hourly rate growth depends on the industry salary growth and is mostly out of your control.</p>

<p>Hitting the ceiling is an amazing achievement though - it can make you one of the top paid professionals in your field. This also makes it is quite unlikely. I am nowhere near the ceiling myself and only a couple of wildly successful freelancers I know of may have glimpsed it from afar. I believe it’s a lot easier to move up the ladder once again than it is to actually hit this ceiling.</p>

<p>But the next level is being a <em>legend</em>. How do you become one? Well, I am no legend either, but I have been in the vicinity of that territory a couple times and I can tell you what I know.</p>

<p>Let’s start with the term “fixed price”. You may have already signed a few high paying fixed budget contracts as an amateur or a pro. But that didn’t necessarily make you a legend. For the purposes of this discussion, most fixed budget contracts are still hourly, you just agreed to calculate it upfront, based on estimations and assumptions. This is one more way to increase your hourly pay if you know what you’re doing, or a way to decrease it, if you don’t.</p>

<p>You are a legend when your compensation is completely detached from, and thus no longer limited by the time and effort you put in. Oh, you still put those in, and they’re worth more than most of your competitors’, but that’s not what you are getting paid for. You are getting paid for the outcome your input brings in for the client.</p>

<p>And so, pros still get paid, albeit handsomely, for turning the screws. Legends get paid for knowing which ones to turn.</p>]]></content><author><name>mzrnsh</name></author><category term="freelancing" /><summary type="html"><![CDATA[Freelancing can be a highly rewarding career path. When I got started just over 5 years ago, I had spent as much time being an employee. Looking back, my freelance years have been more fulfilling, interesting, and profitable than my office years.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mzrn.sh/assets/uploads/freelance-ladder.jpg" /><media:content medium="image" url="https://mzrn.sh/assets/uploads/freelance-ladder.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">5 things you should do as a DIY Shopify merchant</title><link href="https://mzrn.sh/2021/11/27/5-things-you-should-do-as-a-diy-shopify-merchant/" rel="alternate" type="text/html" title="5 things you should do as a DIY Shopify merchant" /><published>2021-11-27T05:26:17+00:00</published><updated>2021-11-27T05:26:17+00:00</updated><id>https://mzrn.sh/2021/11/27/5-things-you-should-do-as-a-diy-shopify-merchant</id><content type="html" xml:base="https://mzrn.sh/2021/11/27/5-things-you-should-do-as-a-diy-shopify-merchant/"><![CDATA[<p>Shopify is a great choice as the e-commerce platform for many types of businesses. This includes stores run by tech-savvy merchants, who prefer to do simple or not-so-simple things themselves instead of hiring a developer.</p>

<p>If you are such a merchant, or a content manager who’s also in charge of technical maintenance, this article will give you a few ideas on how to keep your store fast, secure and easy to maintain.</p>

<h2 id="1-remove-apps-you-no-longer-use">1. Remove apps you no longer use</h2>

<p>Make it a habit to go through the list of apps installed on your store and remove ones that you don’t use any more.</p>

<p>Before clicking the “Delete” button, check if the app has an uninstall guide. If it does, make sure to follow it BEFORE removing the app. This is because if the app has changed your theme, or added metafields to your store, and is responsible enough, they should have a button to clean it all up. And they won’t be able to do it after you have uninstalled the app.</p>

<p>Regular cleanups help you in a couple ways:</p>

<ul>
  <li>Your store is not slowed down by an app you don’t use</li>
  <li>You are not sharing your &amp; your customers’ data with a 3rd party that shouldn’t have access to it</li>
</ul>

<h2 id="2-disable-theme-features-you-dont-need">2. Disable theme features you don’t need</h2>

<p>Themes purchased on Shopify theme market or on 3rd party marketplaces tend to have a lot more features than you will ever need. This is because they are designed to support as many store types as possible. Some of these features can slow down your store. It’s not easy to guess which ones so it’s better to treat every feature as a suspect.</p>

<p>Head to your theme editor, and inspect page sections and theme settings. If you a see a feature you are not using and the theme editor has a button to disable it, click it.</p>

<h2 id="3-back-up-before-changing-code">3. Back up before changing code</h2>

<p>First of all, if you have 0 idea what Liquid, JavaScript, HTML, or CSS is, you probably shouldn’t change anything from the theme’s code editor.</p>

<p>But if you are going to change it, make sure to back up your theme before touching a line of code. With backup I mean duplicating your live theme. Make sure to rename the duplicated theme in a way that indicates when and why this backup was made.</p>

<p>But you shouldn’t be making backups before <em>every</em> change. Rather do it before applying a set of changes. For example installing a new app that requires code changes, or adding a feature according to a guide you found online.</p>

<p>You are limited to 20 non-published themes per store. When you hit that number, you will have to delete some of the old backups to make room for new ones.</p>

<p><em>If you feel adventurous,</em> you can also <a href="https://shopify.dev/themes/tools/github" target="_blank">set up the Shopify + Github sync</a>. This will automatically keep track of every change ever made to your theme. If you manage to complete this successfully, the developer you hire (or become) in future will thank you for it.</p>

<h2 id="4-understand-and-limit-the-permissions-you-grant-others">4. Understand and limit the permissions you grant others</h2>

<p>Chances are high that other parties will need to access your store. Never share your password with anyone and if possible, enable 2-factor authentication on your account. Shopify has multiple ways to share access with different parties and it should be enough for all use cases:</p>

<ul>
  <li>Staff accounts: used for inviting employees</li>
  <li>Collaborator access: used by 3rd party app support or developers</li>
  <li>Private apps: used to give access custom apps built for your store, or to 3rd apps that are not on Shopify App Market</li>
</ul>

<p>Technically, you can invite hired developers as employees, but that will count toward your staff account quota, while collaborators won’t.</p>

<p>All these access types can and should have limited permissions. When at the permissions step during invitation, start by unchecking all options, then check the that are actually needed one by one. You can always grant more permissions later, if you miss something.</p>

<p>Actually, this step shouldn’t be on the list of things DIY merchants should do. EVERY Shopify merchant has to deal with this, so make sure you deal with it the least risky way.</p>

<h2 id="5-style-the-checkout-page">5. Style the checkout page</h2>

<p>The number of stores I see with the default checkout styles is surprising. Now, I am not a marketer who has studied how this affects your conversion rates. But I believe it falls under common sense that different pages of the same website should look similar to avoid confusing its visitors.<br />
<br />
Shopify doesn’t allow changing much on the checkout page. Make sure to make use of what it does. Add your logo, change some colors.. takes 2 minutes.</p>]]></content><author><name>mzrnsh</name></author><category term="shopify" /><category term="speed" /><category term="security" /><summary type="html"><![CDATA[Shopify is a great choice as the e-commerce platform for many types of businesses. This includes stores run by tech-savvy merchants, who prefer to do simple or not-so-simple things themselves instead of hiring a developer.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mzrn.sh/assets/img/avatar_1000x.jpg" /><media:content medium="image" url="https://mzrn.sh/assets/img/avatar_1000x.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How I helped build a profitable MVP over a weekend</title><link href="https://mzrn.sh/2021/11/14/how-I-helped-build-a-profitable-mvp-over-a-weekend/" rel="alternate" type="text/html" title="How I helped build a profitable MVP over a weekend" /><published>2021-11-14T19:57:31+00:00</published><updated>2021-11-14T19:57:31+00:00</updated><id>https://mzrn.sh/2021/11/14/how-I-helped-build-a-profitable-mvp-over-a-weekend</id><content type="html" xml:base="https://mzrn.sh/2021/11/14/how-I-helped-build-a-profitable-mvp-over-a-weekend/"><![CDATA[<p>A couple years ago, a good friend of mine, who was in charge of operations at a food delivery company, got laid off after a merger - they kept the guy from the other side.</p>

<p>He decided to start his own thing. He personally knew and had earned the respect of many restaurant owners in the town. And the delivery guys just loved him. People would follow.</p>

<p>Just one problem: he didn’t have the software or the money to build it. He got a few quotes, all beyond his budget and reached out to me to make sure he wasn’t getting ripped off. He was not. He’d get a website, an admin panel, mobile apps… in 3-4 months. He didn’t have the time either.</p>

<p>So I did what any developer friend would do - I said I could build it over the weekend. It was Friday. Whatever we did that weekend made it possible for him to onboard the first restaurant on Monday. But this story is not about me being a superb engineer, which I am not. It’s about scaling down.</p>

<p>And down we scaled!</p>

<p>When you hear a “food delivery app” you probably imagine something similar to what you already have on your phone: list of restaurants with menus, placing orders with a few taps, tracking how your food moves on the map, tipping couriers and leaving feedback. We didn’t have any of that. We didn’t have push notifications or online payments either. In fact, there was no mobile app at all.</p>

<p>We had to work with some serious, immediate constraints: we only had that weekend as I was working full time back then. And even if we had the time, neither of us knew how to build a mobile app.</p>

<p>So we “scaled down” all the way to B2B. Turns out, more than a few restaurants in town would gladly handle the sales and marketing themselves and keep 100% of the order value minus fixed delivery fees.</p>

<p>All we built was a basic web app with a couple forms, and this is how it all worked:</p>

<ul>
  <li>Restaurants would accept orders by phone, Facebook, Whatsapp, Instagram… Didn’t matter.</li>
  <li>They’d then fill in a form in our app with customer address.</li>
  <li>The app would assign the order to a free courier, triggering an email. Their Gmail was our mobile app!</li>
  <li>The courier would then pick it up, deliver, take cash on delivery, and mark themselves as “available” again.</li>
  <li>At the end of the day the couriers would bring in the cash, minus their cut.</li>
  <li>At the end of the month, my friend would wire the money to the restaurants, minus his cut.</li>
</ul>

<p>This CRUDe (pun intended), ad-hoc and free MVP functioned for over a year, keeping all sides happy and profitable. Later my friend was able to afford building a fully-fledged app and eventually, to exit on his terms.</p>

<p>Each time I start a new project, I remind myself of this story and the important lesson I learned from it. It’s not about making friends with a developer, although having me at his side did help my friend save a few bucks. No, it’s about embracing the constraints. They proved to be his true allies, pushing him towards less features, faster launch and a better business model. Pushing him into profitability from day 0.</p>

<p>When you have the money and the time, it’s tempting to throw them at your problems. That way you have a good chance to get what you expect. But when all you have is your creativity, you might end up with something much better, something you would never expect.</p>]]></content><author><name>mzrnsh</name></author><category term="mvp" /><category term="startup" /><summary type="html"><![CDATA[A couple years ago, a good friend of mine, who was in charge of operations at a food delivery company, got laid off after a merger - they kept the guy from the other side.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mzrn.sh/assets/img/avatar_1000x.jpg" /><media:content medium="image" url="https://mzrn.sh/assets/img/avatar_1000x.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>