<?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: Joe Masilotti</title>
    <description>The latest articles on DEV Community by Joe Masilotti (@joemasilotti).</description>
    <link>https://dev.to/joemasilotti</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%2F72181%2F182f79b1-3933-4d4b-8b01-c11dfa524267.png</url>
      <title>DEV Community: Joe Masilotti</title>
      <link>https://dev.to/joemasilotti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joemasilotti"/>
    <language>en</language>
    <item>
      <title>An open-source reverse job board for Rails developers</title>
      <dc:creator>Joe Masilotti</dc:creator>
      <pubDate>Tue, 08 Feb 2022 17:34:57 +0000</pubDate>
      <link>https://dev.to/joemasilotti/an-open-source-reverse-job-board-for-rails-developers-3mj2</link>
      <guid>https://dev.to/joemasilotti/an-open-source-reverse-job-board-for-rails-developers-3mj2</guid>
      <description>&lt;p&gt;Hi, 👋 I'm &lt;a href="https://masilotti.com"&gt;Joe Masilotti&lt;/a&gt;. I'm building &lt;a href="https://railsdevs.com"&gt;&lt;code&gt;railsdevs&lt;/code&gt;&lt;/a&gt; to make it easier for Ruby on Rails developers to find their next gig.&lt;/p&gt;

&lt;p&gt;The idea is simple: &lt;strong&gt;you post your developer profile and companies reach out &lt;em&gt;to you&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;railsdevs&lt;/code&gt; is being built around three core values:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Empowering the independent developer&lt;/li&gt;
&lt;li&gt;Doing everything in public&lt;/li&gt;
&lt;li&gt;Creating a safe, inclusive environment&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Empowering the developer
&lt;/h2&gt;

&lt;p&gt;I've been an independent developer for the past two years. And leads are rarely consistent. Sometimes I can't keep up with the work and other times I struggle to find my next gig.&lt;/p&gt;

&lt;p&gt;And job boards are rarely helpful for freelance and contract work. They're focused on full-time roles.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;railsdevs&lt;/code&gt; strives to give power back to the independent developer. Instead of companies posting their jobs, developers post their profiles. That way, the power dynamic is reversed as companies have to reach out to developers first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing everything in public
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;railsdevs&lt;/code&gt; is an Open Startup &lt;em&gt;and&lt;/em&gt; open source. It operates fully transparent and shares its metrics, like revenue and traffic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/joemasilotti/railsdevs.com/"&gt;Source code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.usefathom.com/share/cacnfaan/railsdevs.com"&gt;Public analytics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/open"&gt;Revenue and expenses&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.honeybadger.io/project/EKRGgkQdR0"&gt;Bug reports&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of that, I'm making all my decisions in public. I'm posting questions and ideas when I'm looking for feedback on &lt;a href="https://github.com/joemasilotti/railsdevs.com/discussions"&gt;GitHub Discussions&lt;/a&gt;. And I'm tweeting about work in progress (and sneak previews) &lt;a href="https://twitter.com/joemasilotti"&gt;on Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're a freelance Rails developer looking for your next gig then &lt;a href="https://railsdevs.com"&gt;add your profile&lt;/a&gt;! The site is 100% free for developers and is connecting devs like you to paid contracts.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>rails</category>
      <category>career</category>
      <category>startup</category>
    </item>
    <item>
      <title>Hotwire dev content?</title>
      <dc:creator>Joe Masilotti</dc:creator>
      <pubDate>Mon, 21 Jun 2021 16:08:24 +0000</pubDate>
      <link>https://dev.to/joemasilotti/hotwire-dev-content-4fg8</link>
      <guid>https://dev.to/joemasilotti/hotwire-dev-content-4fg8</guid>
      <description>&lt;p&gt;Anyone have any Hotwire dev content they’ve recently published? I’d love to promote it in &lt;a href="https://masilotti.com/hotwire"&gt;my new newsletter&lt;/a&gt;! Stimulus, Turbo (Native), code, courses, thoughts, anything works. Reply below with your links.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>ios</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Turbolinks is dead, long live Turbo ⚡</title>
      <dc:creator>Joe Masilotti</dc:creator>
      <pubDate>Sat, 16 Jan 2021 16:32:03 +0000</pubDate>
      <link>https://dev.to/joemasilotti/turbolinks-is-dead-long-live-turbo-5cd3</link>
      <guid>https://dev.to/joemasilotti/turbolinks-is-dead-long-live-turbo-5cd3</guid>
      <description>&lt;p&gt;Turbolinks, &lt;a href="https://masilotti.com/beermenus-and-turbolinks/"&gt;my favorite way to build hybrid apps&lt;/a&gt;, recently received a huge update.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Turbo(links)?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/turbolinks/turbolinks"&gt;Turbolinks&lt;/a&gt; is a tiny JavaScript library bundled into Rails apps that makes pages load way faster. When a link is clicked, the library takes over and performs the request via AJAX then replaces the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; element with the response.&lt;/p&gt;

&lt;p&gt;Developers can leverage these “hooks” to build hybrid applications based on the mobile web site they already have. There are native iOS and Android components that work with the JavaScript to create native-feeling interactions.&lt;/p&gt;

&lt;p&gt;The native app renders the mobile web view and the frameworks handle the “glue” in between. Think: clicking a link pushes a new controller on the navigation stack. These tools enable small teams of developers to build high-fidelity cross-platform apps. &lt;a href="https://apps.apple.com/us/app/basecamp-3/id1015603248"&gt;Basecamp&lt;/a&gt;, &lt;a href="https://apps.apple.com/us/app/hey-email/id1506603805"&gt;Hey&lt;/a&gt;, &lt;a href="https://apps.apple.com/us/app/zaarly/id964717947"&gt;Zaarly&lt;/a&gt;, &lt;a href="https://apps.apple.com/us/app/beermenus-find-great-beer/id917882057"&gt;BeerMenus&lt;/a&gt;, and more are all Turbolinks-powered hybrid apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turbo and Hotwire
&lt;/h2&gt;

&lt;p&gt;Cosmetically, the framework is now called Turbo and lives under the Hotwire “namespace” both on &lt;a href="https://github.com/hotwired/"&gt;GitHub&lt;/a&gt; and &lt;a href="https://hotwire.dev"&gt;online&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The web side of Turbo saw a slew of new features and existing things to play with. To show how powerful Turbo is, &lt;a href="https://hotwire.dev"&gt;watch DHH create a live-updating chat application&lt;/a&gt; in just 15 minutes. 🤯&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/hotwired/turbo-ios"&gt;iOS component&lt;/a&gt;, called an adapter, received a much smaller update. The &lt;a href="https://github.com/hotwired/turbo-ios/blob/main/Docs/Migration.md"&gt;upgrade path&lt;/a&gt; from Turbolinks is small and relatively painless; I was able to migrate two existing apps in a few hours each.&lt;/p&gt;

&lt;p&gt;The most exciting new feature for the native frameworks is Path Configuration. This moves the URL routing logic to a JSON file. The best part is that this file can live on your server, enabling remote configuration of different URLs. 💪&lt;/p&gt;

&lt;h2&gt;
  
  
  I’m compiling 5 years of Turbo(links) experience 📓
&lt;/h2&gt;

&lt;p&gt;Since I started working with Turbo(links) in 2015, I’ve been noting all the gotchas and rough patches. I’m slowly compiling these into something more formal to share with the public.&lt;/p&gt;

&lt;p&gt;That said, I’m not sure if a book, video course, or blog post series will be the best fit. &lt;strong&gt;If you’re interested in learning more about Turbolinks, how would you prefer to learn?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;P.S. If you need help on your Turbo(links) project ASAP don’t hesitate to &lt;a href="//mailto:joe@masilotti.com"&gt;send me an email&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you feel about hybrid? 🤔
&lt;/h2&gt;

&lt;p&gt;Personally, I find them an amazing middle ground for small teams to manage the three big platforms at the same time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you feel about hybrid?&lt;/strong&gt; I’d love to hear what you think in the comments!&lt;/p&gt;

&lt;h2&gt;
  
  
  More about me
&lt;/h2&gt;

&lt;p&gt;I'm Joe and I write every week on &lt;a href="https://masilotti.com"&gt;Masilotti.com&lt;/a&gt;. I also tweet about this kind of stuff &lt;a href="https://twitter.com/joemasilotti"&gt;on Twitter&lt;/a&gt;. This snippet was part of my newsletter, &lt;a href="https://masilotti.com/newsletter/edition-ii/"&gt;The Masilotti Monthly&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rails</category>
      <category>ios</category>
    </item>
    <item>
      <title>Tips for a better a Product Hunt launch</title>
      <dc:creator>Joe Masilotti</dc:creator>
      <pubDate>Tue, 01 Dec 2020 21:23:15 +0000</pubDate>
      <link>https://dev.to/joemasilotti/tips-for-a-better-a-product-hunt-launch-193h</link>
      <guid>https://dev.to/joemasilotti/tips-for-a-better-a-product-hunt-launch-193h</guid>
      <description>&lt;p&gt;Product Hunt is still a great source of traffic for new (or reworked!) products. While it won't make or break your idea, it does help generate momentum and awareness. Here are some tips I've picked up from my recent launch.&lt;/p&gt;

&lt;h3&gt;
  
  
  What worked
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build in public&lt;/strong&gt; - Weeks before the launch I documented everything I was doing on Twitter. This helped build up an audience that was "primed" to upvote when the launch went live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get early upvotes&lt;/strong&gt; - Product Hunt starts its 24 hour clock at midnight PST. And your first hour is important in getting to the front page. Ask a friend to help promote when it goes live. Don't stay up late - you want to be active during the day!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live tweet&lt;/strong&gt; - Tweet in the morning that you launched and continue to update the thread throughout the day. Talk about big wins (visits, sign ups, sales, etc.) and interesting comments. The more you comment on the thread the more it will re-appear in folks' newsfeeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be genuine&lt;/strong&gt; - Don't ask for upvotes (or comments), ask for &lt;em&gt;support&lt;/em&gt;. You're probably nervous, let folks know! I found this helps folks resonate with why I'm promoting the launch. They will want the same in return when they go live.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What didn't work
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Day-of e-mail campaigns&lt;/strong&gt; - Unless you have a &lt;em&gt;huge&lt;/em&gt; number of subscribers this won't be worth it. Think of it this way: if you have 1000 subscribers, 10% click through, and 10% of those upvote you only looking at a single upvote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter ads&lt;/strong&gt; - I spent $15 and received 2 clicks. Crazy! If I was linking directly to a paid service $7.50 CPA might be worth it, but not for fake internet points.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Obsessively refresh metrics&lt;/strong&gt; - This doesn't help anyone. If the numbers change you get a small dopamine rush. But when they don't the "down" is worse than the "high" - stick to checking on regular intervals (e.g. every hour).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  (BONUS) What could have worked
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Post to Indie Hackers&lt;/strong&gt; - A great community that offers a ton of support to public launched. Somehow this missed my radar and I totally forgot. Don't make the same mistake!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get hunted&lt;/strong&gt; - I couldn't find someone "important" to hunt my product in time. Maybe it would have made a difference in the beginning, but I can't say for sure. My advice is that if you can do it, well, it can't hurt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;P.S. I'm &lt;a href="https://twitter.com/joemasilotti"&gt;Joe Masilotti&lt;/a&gt;, I blog about this kind of stuff on &lt;a href="https://masilotti.com"&gt;Masilotti.com&lt;/a&gt;. The product I launched is &lt;a href="https://mugshotbot.com"&gt;Mugshot Bot&lt;/a&gt;. Cheers!&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A free tool to generate Open Graph images for GitHub repos</title>
      <dc:creator>Joe Masilotti</dc:creator>
      <pubDate>Fri, 20 Nov 2020 14:38:19 +0000</pubDate>
      <link>https://dev.to/joemasilotti/a-free-tool-to-generate-open-graph-images-for-github-repos-45mc</link>
      <guid>https://dev.to/joemasilotti/a-free-tool-to-generate-open-graph-images-for-github-repos-45mc</guid>
      <description>&lt;p&gt;I made a free tool that generates Open Graph images for GitHub repos.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.mugshotbot.com/github" rel="noopener noreferrer"&gt;mugshotbot.com/github&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I work in open source and was tired of sharing ugly links on Twitter and Facebook. This is how they usually look.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F0ybwisg9pqdtaofu0gxu.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F0ybwisg9pqdtaofu0gxu.jpeg" alt="Boring Twitter preview of a GitHub repo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One day I realized GitHub lets you upload a custom image, so I started making some templates. I figured I could work it into &lt;a href="https://www.mugshotbot.com" rel="noopener noreferrer"&gt;Mugshot Bot&lt;/a&gt; and use the GitHub API.&lt;/p&gt;

&lt;p&gt;That eventually turned into a full product! It’s free to use. And it can create some nice Open Graph images for your open source projects.&lt;/p&gt;

&lt;p&gt;Here's what that same repo looks like now. 😻&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Frtuykzro2jtnaxpbh4fp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Frtuykzro2jtnaxpbh4fp.png" alt="Improved Twitter preview of a GitHub repo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I use the GitHub API to pull in a few stats from the repo:&lt;/p&gt;

&lt;p&gt;🌟 stars&lt;br&gt;
🌍 language&lt;br&gt;
🍴 forks&lt;br&gt;
❗ open issues&lt;br&gt;
👤 creator avatar&lt;/p&gt;

&lt;p&gt;There's three different themes and you can customize the color and background pattern. &lt;a href="https://dev.to/joemasilotti/i-ll-make-a-social-image-for-your-best-github-repo-4g4g"&gt;Earlier this week&lt;/a&gt; a few folks helped me beta test - here's what they created!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fnzzo3hifpzd156xq3w18.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fnzzo3hifpzd156xq3w18.png" alt="Four examples of social previews for GitHub repos via Mugshot Bot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you give it a try, reply with your repo and I'll check it out. Happy hacking folks! 👋&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>github</category>
      <category>twitter</category>
      <category>design</category>
    </item>
    <item>
      <title>Regexes with multiple slashes in Ruby</title>
      <dc:creator>Joe Masilotti</dc:creator>
      <pubDate>Thu, 19 Nov 2020 15:08:41 +0000</pubDate>
      <link>https://dev.to/joemasilotti/regexes-with-multiple-slashes-in-ruby-1c4p</link>
      <guid>https://dev.to/joemasilotti/regexes-with-multiple-slashes-in-ruby-1c4p</guid>
      <description>&lt;p&gt;I picked up a new tip yesterday while working with regexes in Ruby.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;tl;dr - Use &lt;code&gt;%r{}&lt;/code&gt; over &lt;code&gt;/.../&lt;/code&gt; when matching regexes with more than one &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was testing if a string begins with &lt;code&gt;http://&lt;/code&gt; or &lt;code&gt;https://&lt;/code&gt; and wrote a small regex.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="sr"&gt;/^https?:\/\//&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Broken down, this ensures the string:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Starts with &lt;code&gt;http&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The next character is optionally &lt;code&gt;s&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The next characters are  &lt;code&gt;://&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even for such a simple regex that feels a bit hard for me to read. There are too many escaped slashes for my taste.&lt;/p&gt;

&lt;p&gt;To improve this a bit you can use &lt;code&gt;r%{}&lt;/code&gt; over &lt;code&gt;/.../&lt;/code&gt;. The syntax works the same but you don’t need to escape slashes.&lt;/p&gt;

&lt;p&gt;The regex becomes something much more readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="sr"&gt;%r{^https?://}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub's RuboCop styleguide has a recommendation on when to use this syntax.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use &lt;code&gt;%r&lt;/code&gt; only for regular expressions matching &lt;em&gt;more than&lt;/em&gt; one ‘/‘ character. - &lt;a href="https://github.com/github/rubocop-github/blob/master/STYLEGUIDE.md#regular-expressions"&gt;RubuCop Ruby Style Guide&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;P.S. I could have probably just used two &lt;code&gt;#starts_with?&lt;/code&gt; calls but where’s the fun in that?&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I'll make a custom social image for your GitHub repo</title>
      <dc:creator>Joe Masilotti</dc:creator>
      <pubDate>Mon, 16 Nov 2020 17:50:32 +0000</pubDate>
      <link>https://dev.to/joemasilotti/i-ll-make-a-social-image-for-your-best-github-repo-4g4g</link>
      <guid>https://dev.to/joemasilotti/i-ll-make-a-social-image-for-your-best-github-repo-4g4g</guid>
      <description>&lt;p&gt;Hey folks! I'm building an image generation tool that automates link previews.&lt;/p&gt;

&lt;p&gt;Later this week I'm launching a new feature. But I wanted to give the DEV community a sneak peek!&lt;/p&gt;

&lt;p&gt;Send me a link to your best GitHub repo and I'll create a custom social image.&lt;/p&gt;

&lt;p&gt;You can upload this on the settings page of the repo - it will be used when you share on Twitter, Facebook, LinkedIn, etc.&lt;br&gt;
Feel free to ask for a specific color or hex code!&lt;/p&gt;

&lt;p&gt;Here are some examples&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F44t86m9dpjencoo9j1rx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F44t86m9dpjencoo9j1rx.png" alt="GitHub social images made with Mugshot Bot"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>design</category>
      <category>twitter</category>
    </item>
    <item>
      <title>How I built a URL to image generator over the weekend</title>
      <dc:creator>Joe Masilotti</dc:creator>
      <pubDate>Mon, 31 Aug 2020 20:32:03 +0000</pubDate>
      <link>https://dev.to/joemasilotti/how-i-built-a-url-to-image-generator-over-the-weekend-47k7</link>
      <guid>https://dev.to/joemasilotti/how-i-built-a-url-to-image-generator-over-the-weekend-47k7</guid>
      <description>&lt;p&gt;I've grown really tired of manually creating social images for every single blog post. They take way too long to create and online tools always end up looking too generic. How many stock photos can I scroll through before they all start to look the same?&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://www.mugshotbot.com"&gt;Mugshot Bot&lt;/a&gt;. An automated, zero effort social image generator. You pass it a URL and it generates a perfectly sized, unique, beautiful social image.&lt;/p&gt;

&lt;p&gt;Here's what they look like! The color and background pattern are randomized from a hand-tuned selection. The title and subtitle come directly from the HTML.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TnuPacAN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.mugshotbot.com/m%3Furl%3Dhttps://masilotti.com/great-two-player-board-games/" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TnuPacAN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.mugshotbot.com/m%3Furl%3Dhttps://masilotti.com/great-two-player-board-games/" alt="Example Mugshot Bot image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Overall approach
&lt;/h2&gt;

&lt;p&gt;My goal is to design in HTML and CSS and then convert it to a PNG. This worked pretty well with some &lt;code&gt;wkhtmlto*&lt;/code&gt; magic but there were a few hoops I had to jump through. Here's what I did.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fetch the content
&lt;/h3&gt;

&lt;p&gt;All of the content comes directly from the URL's HTML. So the first step is to fetch the website and parse the DOM. I'm using &lt;code&gt;HTTParty&lt;/code&gt; and &lt;code&gt;Nokogiri&lt;/code&gt; and then looking for specific markup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HTTParty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;
&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Nokogiri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"meta[property='og:title']"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"meta[property='og:description']"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  Render and style the HTML
&lt;/h3&gt;

&lt;p&gt;Now that we have the copy we can drop it into some HTML. In Rails we can render an arbitrary view and pass in some variables via &lt;code&gt;ApplicationController#render&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;mugshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Mugshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rendered_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"mugshots/show"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;assigns: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="ss"&gt;formats: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:html&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 rendered HTML uses the default layout so we have all of the CSS and fonts normally added in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convert to an image
&lt;/h2&gt;

&lt;p&gt;Where the magic happens: &lt;code&gt;wkhtmlto*&lt;/code&gt;. Or, as it is usually known, &lt;code&gt;wkhtmltopdf&lt;/code&gt;. This library is bundled with a lesser known tool &lt;code&gt;wkhtmltoimage&lt;/code&gt; that does exactly what we need.&lt;/p&gt;

&lt;p&gt;If you have the library installed you can call directly into it with &lt;code&gt;Open3&lt;/code&gt;. This works a bit better than backticks because you can handle stderr.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Open3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"wkhtmltoimage jpeg - -"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;stdin_data: &lt;/span&gt;&lt;span class="n"&gt;rendered_html&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The two dashes (&lt;code&gt;- -&lt;/code&gt;) at the end of the command tell the tool to render &lt;em&gt;from&lt;/em&gt; stdin and render &lt;em&gt;to&lt;/em&gt; stdout. &lt;code&gt;Open3&lt;/code&gt; will write stdout to &lt;code&gt;result&lt;/code&gt; and &lt;code&gt;stderr&lt;/code&gt; to &lt;code&gt;error&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Render from the controller
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;result&lt;/code&gt; is the actual image, as data. We can render this directly from the controller. Ideally, this would be uploaded to S3 and/or put behind a CDN.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="n"&gt;send_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"image/jpeg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;disposition: &lt;/span&gt;&lt;span class="s2"&gt;"inline"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  What a weekend!
&lt;/h2&gt;

&lt;p&gt;Thanks for reading, I hope you enjoyed how I built a little side project over the weekend.&lt;/p&gt;

&lt;p&gt;If you give &lt;a href="https://www.mugshotbot.com"&gt;Mugshot Bot&lt;/a&gt; a try please let me know what you think in the comments! I'm open to feature requests, too.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rails</category>
      <category>ruby</category>
      <category>design</category>
    </item>
    <item>
      <title>This past weekend, I built a landing page and redesigned my blog with Tailwind CSS. And I don't think I'll ever go back.</title>
      <dc:creator>Joe Masilotti</dc:creator>
      <pubDate>Fri, 24 Jul 2020 21:21:30 +0000</pubDate>
      <link>https://dev.to/joemasilotti/this-past-weekend-i-built-a-landing-page-and-redesigned-my-blog-with-tailwind-css-and-i-don-t-think-i-ll-ever-go-back-44b1</link>
      <guid>https://dev.to/joemasilotti/this-past-weekend-i-built-a-landing-page-and-redesigned-my-blog-with-tailwind-css-and-i-don-t-think-i-ll-ever-go-back-44b1</guid>
      <description>&lt;p&gt;This past weekend, I built &lt;a href="https://xwing.app" rel="noopener noreferrer"&gt;a landing page&lt;/a&gt; and redesigned &lt;a href="https://masilotti.com" rel="noopener noreferrer"&gt;Masilotti.com&lt;/a&gt; with Tailwind CSS. Getting going was tough. But I don't think I'll ever go back.&lt;/p&gt;

&lt;p&gt;Oh, and both the &lt;a href="https://github.com/joemasilotti/x-wing-ai-tailwind" rel="noopener noreferrer"&gt;landing page&lt;/a&gt; and &lt;a href="https://github.com/joemasilotti/masilotti.com-tailwind" rel="noopener noreferrer"&gt;Masilotti.com&lt;/a&gt; are now open source!&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/joemasilotti" rel="noopener noreferrer"&gt;
        joemasilotti
      &lt;/a&gt; / &lt;a href="https://github.com/joemasilotti/masilotti.com" rel="noopener noreferrer"&gt;
        masilotti.com
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Source for masilotti.com, built with Bridgetown and Tailwind CSS.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  What the h*ck is Tailwind?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://tailwindcss.com" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt; is a CSS framework for people who hate CSS frameworks. - Joe Masilotti&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tailwind takes an unopinionated approach to design and leaves everything in your control. Instead of being tied to, say, Twitter Bootstrap’s card design, you build yours from scratch with Tailwind.&lt;/p&gt;

&lt;p&gt;And from someone who usually hates working with CSS, it's actually quite enjoyable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Another CSS framework?!
&lt;/h2&gt;

&lt;p&gt;Yes. And no. But mostly yes.&lt;/p&gt;

&lt;p&gt;Tailwind’s unique approach focuses on utility classes to build everything. Instead of given components like headers, heroes, or cards, you instead use tiny little classes that do a single thing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mr-4&lt;/code&gt; adds a bit of &lt;strong&gt;m&lt;/strong&gt;argin to the &lt;strong&gt;r&lt;/strong&gt;ight. &lt;code&gt;bg-gray-200&lt;/code&gt; sets the background color to a very light gray. And &lt;code&gt;flex&lt;/code&gt; sets the display property.&lt;/p&gt;

&lt;p&gt;Combining these with size class modifiers enables very powerful styling with very little code. All classes apply to all size classes until you prefix them. &lt;code&gt;md:max-w-xl&lt;/code&gt; sets the max width to extra large but only on medium and larger devices. &lt;code&gt;lg:h-16&lt;/code&gt; sets the height on large (and larger) devices. While a plain &lt;code&gt;pl-16&lt;/code&gt; will set a &lt;strong&gt;l&lt;/strong&gt;eft &lt;strong&gt;p&lt;/strong&gt;adding on all sizes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mx-4 md:mx-8 lg:mx-16 bg-teal-200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This snippet will have an increasingly bigger horizontal margin as the screen size is increased. And all will use this light teal background color.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;del&gt;Cascading&lt;/del&gt; stylesheets
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fbpdmlc6li0ru29t4jutq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fbpdmlc6li0ru29t4jutq.png" alt="Waterfall with a cross through it"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another goal of Tailwind is to make it easier to deal with large CSS codebases.&lt;/p&gt;

&lt;p&gt;Changing a global style too often cascades across multiple widgets, components, pages, or templates. (Or you miss a part of the selector chain and nothing happens.) With Tailwind, those changes are scoped only to the HTML element to which they are applied.&lt;/p&gt;

&lt;p&gt;In practice, this means you can tweak the layout of your blog without having it mess up your landing page. Enforcing changes that are more direct and easier to reason about.&lt;/p&gt;
&lt;h2&gt;
  
  
  Configuration over convention
&lt;/h2&gt;

&lt;p&gt;A major goal of Tailwind is to write less CSS. Take a second and let that one sink in. A CSS framework that wants you to write less CSS. Crazy!&lt;/p&gt;

&lt;p&gt;As shown above, you create custom styles by combining long chains of utility classes. You don't create global "hero" components that end up getting customized each time they are used anyway.&lt;/p&gt;

&lt;p&gt;On Masilotti.com I have no custom CSS. Zero. Everything comes from Tailwind utility classes and some basic configuration.&lt;/p&gt;

&lt;p&gt;This takes out the biggest gripe I have with CSS, maintaining it. You don't have to maintain something if it doesn't exist in the first place!&lt;/p&gt;
&lt;h2&gt;
  
  
  So, how does it all work?
&lt;/h2&gt;

&lt;p&gt;Another differentiating factor of Tailwind is &lt;em&gt;how&lt;/em&gt; it’s built. If you grab the latest from the CDN, you’ll notice the file is quite large. And there’s a &lt;em&gt;ton&lt;/em&gt; of stuff you will never use. Tailwind has a surprisingly elegant solution for this.&lt;/p&gt;

&lt;p&gt;First, all of the custom Tailwind is run through a preprocessor, usually PostCSS. This does all the fancy iterating and looping to create every single combination of screen size, property, attribute, modifier, etc. That’s why the CDN file is so large.&lt;/p&gt;

&lt;p&gt;The second phase (hint: where the magic happens) is done with CSS purging. This step removes all of CSS classes that aren’t being used in your project. These reductions can be &lt;em&gt;ginormous&lt;/em&gt;. A smaller bundle means a smaller payload and easier parsing for the web. Faster load times means happier customers!&lt;/p&gt;
&lt;h2&gt;
  
  
  With great power comes great responsibility
&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fia4fmcol8wqadk195ixf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fia4fmcol8wqadk195ixf.png" alt="Spiderman saying "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tailwind gives you the flexibility to create anything you want. Like, literally anything. Take a peek at their CSS reset, &lt;a href="https://tailwindcss.com/docs/preflight/" rel="noopener noreferrer"&gt;Preflight&lt;/a&gt;. It even resets the font size of heading tags!&lt;/p&gt;

&lt;p&gt;To compare, Twitter Bootstrap makes it really easy to create something that looks &lt;em&gt;fine&lt;/em&gt;. Throw a grid up, slap some cards in place, maybe tweak the hero, and boom. You have something that functions and doesn’t look too terrible.&lt;/p&gt;

&lt;p&gt;Tailwind, out of the box, looks like entirely unstyled HTML. And it takes a bit of work to get it looking decent. But the investment pays for itself very quickly.&lt;/p&gt;

&lt;p&gt;To work around this I've adopted the official &lt;a href="https://tailwindcss.com/docs/preflight/" rel="noopener noreferrer"&gt;typography&lt;/a&gt; plugin. Install it and wrap blocks of prose in a &lt;code&gt;prose&lt;/code&gt; class for very sensible defaults. This content of Masilotti.com is 99% styled with the &lt;code&gt;typography&lt;/code&gt; plugin.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building Tailwind
&lt;/h2&gt;

&lt;p&gt;OK, great, you’re convinced. “Let’s do Tailwind!” I can hear you yelling at your monitor. But how? …great question.&lt;/p&gt;

&lt;p&gt;For all my praise, Tailwind is still a pain in the butt to build. Because of its complex build process (relative to Bootstrap) it requires a bit of modern web packaging know-how.&lt;/p&gt;

&lt;p&gt;There are more than a few ways to get Tailwind integrated into your build process. I’ll touch on the two that I dove into this weekend. There’s also plenty of &lt;a href="https://github.com/tailwindlabs/tailwindcss-setup-examples" rel="noopener noreferrer"&gt;official example setups on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Static HTML site
&lt;/h3&gt;

&lt;p&gt;My first goal with Tailwind was building a &lt;a href="https://xwing.app" rel="noopener noreferrer"&gt;static HTML landing page&lt;/a&gt;. This let me dive into manually setting up the build process along with all of the CSS processing.&lt;/p&gt;

&lt;p&gt;I ended up with a fairly standard configuration and two custom yarn scripts to help with watching during development and deploying for production.&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="c1"&gt;// package.json&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scripts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;build&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;copyfiles --up 1 'src/**/*' dist &amp;amp;&amp;amp; postcss css/base.css -o dist/css/styles.css --env production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;watch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postcss css/base.css -o src/css/styles.css --watch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&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="c1"&gt;// postcss.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postcss-import&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tailwindcss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;autoprefixer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@fullhuman/postcss-purgecss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./dist/index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;defaultExtractor&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;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Za-z0-9-_:&lt;/span&gt;&lt;span class="se"&gt;/]&lt;/span&gt;&lt;span class="sr"&gt;+/g&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="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cssnano&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
      &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;yarn run watch&lt;/code&gt; processes my source CSS and spits it out to be linked in the HTML file. Appending &lt;code&gt;--watch&lt;/code&gt; ensures that &lt;code&gt;postcss&lt;/code&gt; continues to build while files are updated.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yarn run build&lt;/code&gt; copies everything from &lt;code&gt;src/&lt;/code&gt; and moves it to &lt;code&gt;dist/&lt;/code&gt; then processes, purges, and minifies the CSS. I can then deploy the contents of &lt;code&gt;dist/&lt;/code&gt; to production.&lt;/p&gt;

&lt;p&gt;Was it worth doing manually? Yes. Would I do it again? No.&lt;/p&gt;

&lt;p&gt;It was worth learning how the sausage was made but only once. In the future I would copy this over from an old project instead of creating it from scratch.&lt;/p&gt;

&lt;p&gt;P.S. The site is &lt;a href="https://github.com/joemasilotti/x-wing-ai-tailwind" rel="noopener noreferrer"&gt;open source&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Jekyll and Tailwind
&lt;/h3&gt;

&lt;p&gt;This site is built with &lt;a href="https://jekyllrb.com" rel="noopener noreferrer"&gt;Jekyll&lt;/a&gt;, a static site generator written in Ruby. I based the project on &lt;a href="https://github.com/mhanberg/jekyll-tailwind-starter" rel="noopener noreferrer"&gt;jekyll-tailwind-starter&lt;/a&gt; and customized to my liking.&lt;/p&gt;

&lt;p&gt;I don’t think I can recommend using this repo. There are better ones on the official site. Things were a little out of date but the scaffolding was there.&lt;/p&gt;

&lt;p&gt;That said, the repo did get me where I needed to be. It has all of the standard CSS processing built in and integrated with Jekyll’s build scripts. All I have to do is run &lt;code&gt;bin/start&lt;/code&gt; and it watches for changes. It even hooks up &lt;code&gt;live-reload&lt;/code&gt; so I don’t have to refresh the page!&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;jekyll serve &lt;span class="nt"&gt;--livereload&lt;/span&gt; &lt;span class="nt"&gt;--drafts&lt;/span&gt; &lt;span class="nt"&gt;--future&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 5000 &lt;span class="nt"&gt;--livereload_port&lt;/span&gt; 35729 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Masilotti.com is open source so feel free to poke around!&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/joemasilotti" rel="noopener noreferrer"&gt;
        joemasilotti
      &lt;/a&gt; / &lt;a href="https://github.com/joemasilotti/masilotti.com" rel="noopener noreferrer"&gt;
        masilotti.com
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Source for masilotti.com, built with Bridgetown and Tailwind CSS.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>css</category>
      <category>design</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Just Deleted Thousands of Records from Production 😬</title>
      <dc:creator>Joe Masilotti</dc:creator>
      <pubDate>Wed, 20 Mar 2019 14:03:41 +0000</pubDate>
      <link>https://dev.to/joemasilotti/i-just-deleted-thousands-of-records-from-production--4bh7</link>
      <guid>https://dev.to/joemasilotti/i-just-deleted-thousands-of-records-from-production--4bh7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post originally appeared on &lt;a href="http://masilotti.com/deleted-records-from-prod/"&gt;Masilotti.com&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I woke up yesterday eager to add pretty social shares to my side project, &lt;a href="https://www.wetabletop.com"&gt;weTabletop&lt;/a&gt;. As I plugged a few URLs into the &lt;a href="https://cards-dev.twitter.com/validator"&gt;Twitter Card Validator&lt;/a&gt; a few started to 404. Oddly, the records seemed to be fine in my development database running a production dump from late last week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time to Sanity Check
&lt;/h2&gt;

&lt;p&gt;Something was going on. Did I somehow delete a bunch of records from production?&lt;/p&gt;

&lt;p&gt;To verify I remoted in to the Heroku Rails console and started debugging. Comparing table counts with those in dev taught me that the &lt;code&gt;events&lt;/code&gt; table was out of sync. By a lot.&lt;/p&gt;

&lt;p&gt;I figured out &lt;em&gt;which&lt;/em&gt; records were missing by comparing the IDs of all records in production to a normal database sequence (1, 2, 3, 4, etc.). I very rarely actually delete events, so any gaps in the pattern will show something wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;irb&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="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_a&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2526&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;I had somehow lost over 2500 records. 😭&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups? What Backups?
&lt;/h2&gt;

&lt;p&gt;Ideally, this would be fixed by merging a database backup into the current dataset in production. I could have downloaded a dump from a week ago, found all the records, and uploaded those back to prod.&lt;/p&gt;

&lt;p&gt;However, to keep Heroku expenses as low as possible I don't pay for the &lt;a href="https://elements.heroku.com/addons/heroku-postgresql"&gt;$50 database add-on&lt;/a&gt;, the cheapest one that supports automatic backups. So no automated backups for me. 🤠&lt;/p&gt;

&lt;p&gt;Luckily, my dataset is still very small (~30k records) so I can pull production into development every now and then. And my most recent dump was from before the random deletions started occurring! While not perfect, I figured I can write a script to manually export and import the records from development to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export/Import Raw JSON
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Get the IDs of missing records (from before)&lt;/li&gt;
&lt;li&gt;Write each record to a file, as JSON&lt;/li&gt;
&lt;li&gt;Upload the file and an import script to production&lt;/li&gt;
&lt;li&gt;Parse each line and &lt;code&gt;#create!&lt;/code&gt; a new record&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Exporting the &lt;em&gt;entire&lt;/em&gt; record to JSON (bypassing any custom serializers) ensures all the data is preserved. This includes the record's ID and timestamps, something you usually don't want to carry over when moving data.&lt;/p&gt;

&lt;p&gt;This is one of the rare times you actually want that data. I needed the backups to look just like the records they were restoring. Event #412 should be marked as created on Feb 14, not Mar 18 (today).&lt;/p&gt;

&lt;h3&gt;
  
  
  The exporter, run in development
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EventJSONExporter&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;export&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt; &lt;span class="c1"&gt;# pasted in from before&lt;/span&gt;

    &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"db/events.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"wb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find_each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  The importer task, run in production
&lt;/h3&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:events&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s2"&gt;"Import deleted events from JSON file."&lt;/span&gt;
  &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;import_json: :environment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;saved_event_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt; &lt;span class="s2"&gt;"db"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"events.json"&lt;/span&gt;

    &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each_line&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="k"&gt;begin&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save!&lt;/span&gt;
        &lt;span class="n"&gt;saved_event_ids&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;
      &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ParserError&lt;/span&gt;
        &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Couldn't parse JSON: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;StandardError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Couldn't save event &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Imported &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;saved_event_ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; events:"&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;saved_event_ids&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  Root Cause Analysis
&lt;/h2&gt;

&lt;p&gt;None of this matters if the records continue to magically delete themselves. A rigorous seach for &lt;code&gt;destroy&lt;/code&gt; and &lt;code&gt;delete&lt;/code&gt; through the entire codebase lead me to a single culprit: the Google Calendar event importer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GoogleCalendarEventImporter&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_or_update_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;google_calendar_event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_or_initialize_by&lt;/span&gt; &lt;span class="ss"&gt;i_cal_uid: &lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;i_cal_uid&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;google_calendar_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelled?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persisted?&lt;/span&gt;
      &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Looks fairly innocent, right? If the Google Calendar event was cancelled then delete it from the database.&lt;/p&gt;

&lt;p&gt;Turns out &lt;code&gt;i_cal_uid&lt;/code&gt; can be &lt;code&gt;nil&lt;/code&gt; when the event is cancelled. Only ~10% of all events are from Google Calendar, the other 90% never get an &lt;code&gt;i_cal_uid&lt;/code&gt;! This leads to &lt;code&gt;#find_or_initialize_by&lt;/code&gt; finding &lt;em&gt;any&lt;/em&gt; of the other 90% of the events and deleting that one. And this code is run every time a synced calendar is updated — a lot.&lt;/p&gt;

&lt;p&gt;In their defense, &lt;a href="https://developers.google.com/calendar/v3/reference/events"&gt;Google does document&lt;/a&gt; that the &lt;code&gt;iCalUID&lt;/code&gt; can be blank for cancelled events. However, it was noted under the &lt;code&gt;status&lt;/code&gt; section, not where I expected it near the &lt;code&gt;iCalUID&lt;/code&gt; reference.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;status&lt;/strong&gt;: Deleted events are only guaranteed to have the id field populated.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Fixes and Looking Forward
&lt;/h2&gt;

&lt;p&gt;The quick fix is to &lt;em&gt;not&lt;/em&gt; delete the record when the Google Calendar event is cancelled. I made this change and deployed to ensure I didn't lose any more data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_or_update_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;google_calendar_event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;google_calendar_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelled?&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="c1"&gt;# TODO: Remove cancelled events by e.id, not iCalID&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;But this event is still, well, cancelled. It shouldn't be shown to anyone. The code will need to additionally track the Google event ID for each record and only delete if there is a match.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What's the difference between &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;i_cal_uid&lt;/code&gt;?&lt;/strong&gt; Every event has a unique &lt;code&gt;id&lt;/code&gt; per calender and repeating events all share the same &lt;code&gt;i_cal_uid&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How to Prevent This
&lt;/h2&gt;

&lt;p&gt;Phew. In the end I restored all but 17 records; I'll have to manually re-create those myself. But still, I never want to have to do this again. Here are some ways this could have been avoided:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A better understanding of the API contract with Google&lt;/li&gt;
&lt;li&gt;More aggressive alerting when destructive actions occur&lt;/li&gt;
&lt;li&gt;Better unit tests that handle when &lt;code&gt;i_cal_uid&lt;/code&gt; is &lt;code&gt;nil&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At best this post helps someone recover from a similiar data loss. At worst it shows how easily a full-time developer with almost a decade of experience can make such a huge mistake!&lt;/p&gt;

&lt;p&gt;Enjoy my humility but please spend the $50 for a database with a backup strategy. 🙏&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
    </item>
  </channel>
</rss>
