<?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>Forem: Andy Leverenz</title>
    <description>The latest articles on Forem by Andy Leverenz (@justalever).</description>
    <link>https://forem.com/justalever</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%2F104966%2F26ebddce-3b1e-4454-a4f4-ac03b189fe3b.jpg</url>
      <title>Forem: Andy Leverenz</title>
      <link>https://forem.com/justalever</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/justalever"/>
    <language>en</language>
    <item>
      <title>Infinite Scroll with Rails and Turbo - No JavaScript Required!</title>
      <dc:creator>Andy Leverenz</dc:creator>
      <pubDate>Mon, 26 Feb 2024 21:18:46 +0000</pubDate>
      <link>https://forem.com/justalever/infinite-scroll-with-rails-and-turbo-no-javascript-required-35o2</link>
      <guid>https://forem.com/justalever/infinite-scroll-with-rails-and-turbo-no-javascript-required-35o2</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/F63GgJSYE9I"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In this blog post, I will walk you through creating an infinite scroll feature using Rails and Turbo. I wanted to see if using only Turbo was an option for this feature, and it turns out it is. I was excited to discover this, so I wanted to share the wealth.&lt;/p&gt;

&lt;p&gt;I will be sure to cover the necessary steps, including setting up the database and handling the server-side logic. By the end of this guide, you will have a fully functional infinite scroll feature in your Rails application that you can borrow on your own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Make sure you’re using the latest version of Rails and &lt;code&gt;turbo-rails&lt;/code&gt; gem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a new app
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new infinite_scroll_turbo &lt;span class="nt"&gt;-j&lt;/span&gt; esbuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add Tailwind CSS, will_paginate, and faker gems
&lt;/h3&gt;

&lt;p&gt;To make this demo more realistic, I’ll use the &lt;code&gt;tailwindcss-rails&lt;/code&gt; gem for some ready-to-go styles out of the box.&lt;/p&gt;

&lt;p&gt;If you'd like more control consider passing &lt;code&gt;-c tailwind&lt;/code&gt; in your &lt;code&gt;rails new&lt;/code&gt; command which gives you a blank slate OR if you want some UI done for you check out my project &lt;a href="https://railsui.com"&gt;Rails UI&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add tailwindcss-rails faker will_paginate
rails tailwindcss:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Generate some records
&lt;/h3&gt;

&lt;p&gt;First, I’ll set up the development database and house some dummy content we’ll generate with the &lt;a href="https://github.com/faker-ruby/faker"&gt;faker&lt;/a&gt; gem we just installed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails generate scaffold Post title:string content:text
rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add some Ruby code to seed data more quickly inside &lt;code&gt;db/seeds.rb&lt;/code&gt;&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="c1"&gt;# db/seeds.rb&lt;/span&gt;
&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="no"&gt;Faker&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Lorem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;word_count: &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="no"&gt;Faker&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Lorem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paragraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;sentence_count: &lt;/span&gt;&lt;span class="mi"&gt;4&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;p&gt;Finally, run the seed command to generate those records.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails db:seed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Update Rails routing
&lt;/h3&gt;

&lt;p&gt;Since we scaffolded a &lt;code&gt;Post&lt;/code&gt; model, I'll root the app to &lt;code&gt;posts#index&lt;/code&gt;&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="c1"&gt;# config/routes.rb&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;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:posts&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"up"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"rails/health#show"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :rails_health_check&lt;/span&gt;
  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="s2"&gt;"posts#index"&lt;/span&gt; &lt;span class="c1"&gt;# uncomment this line!&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With our database and seeded data in the app, we’re ready to consider the controller logic.&lt;/p&gt;

&lt;p&gt;To make infinite scrolling work, we’ll need some form of pagination.&lt;/p&gt;

&lt;p&gt;I reached for &lt;a href="https://github.com/mislav/will_paginate"&gt;will_paginate&lt;/a&gt; because it’s super simple to set up and use. There are many more options for pagination with Rails, so feel free to swap for something you prefer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update the posts controller
&lt;/h3&gt;

&lt;p&gt;In the &lt;code&gt;PostsController,&lt;/code&gt; I added the following code.&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="c1"&gt;# app/controllers/posts_controller.rb &lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
  &lt;span class="vi"&gt;@posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:page&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;per_page: &lt;/span&gt;&lt;span class="mi"&gt;4&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;h3&gt;
  
  
  Infinite scroll logic with Rails and Turbo
&lt;/h3&gt;

&lt;p&gt;To use no JavaScript we’ll use a unique way to render a &lt;code&gt;index.turbo_stream.erb&lt;/code&gt; file in the form of a turbo stream response. Here's the code in my &lt;code&gt;posts/index.html.erb&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&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;"w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Posts&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"New post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_post_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"posts"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"posts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;src: &lt;/span&gt;&lt;span class="n"&gt;posts_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;format: :turbo_stream&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;loading: :lazy&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next_page&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"load_more"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;src: &lt;/span&gt;&lt;span class="n"&gt;posts_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="vi"&gt;@posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;format: :turbo_stream&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;loading: :lazy&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"fixed bottom-0 rounded-tl right-0 h-12 bg-gray-50/50 backdrop-blur-sm w-full text-lg font-medium py-2 text-gray-900 text-center border-t border-l border-gray-200/80 w-56"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;🚀 Current page: &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"current_page"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_page&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the &lt;code&gt;turbo_frame_tag&lt;/code&gt; we can source a defined path in the Rails app to load lazily. This &lt;strong&gt;just works&lt;/strong&gt; which is impressive in itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-time infinite pagination
&lt;/h3&gt;

&lt;p&gt;I passed an explicit &lt;code&gt;format&lt;/code&gt; to the path helpers (i.e. &lt;code&gt;posts_path(format: :turbo_stream)&lt;/code&gt; ).&lt;/p&gt;

&lt;p&gt;Doing this instructs the rails app to return &lt;code&gt;index.turbo_stream.erb&lt;/code&gt; view logic instead of the default &lt;code&gt;index.html.erb&lt;/code&gt; file using the advertised “HTML over the wire” approach.&lt;/p&gt;

&lt;p&gt;Create a new file called &lt;code&gt;index.turbo_stream.erb&lt;/code&gt; in your &lt;code&gt;app/views/posts&lt;/code&gt; folder. Inside the &lt;code&gt;index.turbo_stream.erb&lt;/code&gt; file, I added the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt; &lt;span class="s2"&gt;"posts"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"posts/post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;collection: &lt;/span&gt;&lt;span class="vi"&gt;@posts&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next_page&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt; &lt;span class="s2"&gt;"load_more"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"load_more"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;src: &lt;/span&gt;&lt;span class="n"&gt;posts_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="vi"&gt;@posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;format: :turbo_stream&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;loading: :lazy&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt; &lt;span class="s2"&gt;"current_page"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_page&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using will_paginate, we can conditionally check for a &lt;code&gt;next_page&lt;/code&gt; based on the parameters set in the controller.&lt;/p&gt;

&lt;p&gt;If a new page is present, I display the &lt;code&gt;turbo_frame_tag “load_more”&lt;/code&gt; in the &lt;code&gt;index.html.erb&lt;/code&gt; and &lt;code&gt;index.turbo_stream.erb&lt;/code&gt; views.&lt;/p&gt;

&lt;p&gt;This dynamically loads via the paths passed to the &lt;code&gt;src&lt;/code&gt; argument on the &lt;code&gt;turbo_frame_tag&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Notice all formats are loaded as &lt;code&gt;:turbo_stream&lt;/code&gt;. This continues happening recursively until no pages are left as you scroll.&lt;/p&gt;

&lt;p&gt;To make each post more resemble a blog post, I updated the markup and styling slightly inside &lt;code&gt;_post.html.erb&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-semibold tracking-tight"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-5 text-lg text-gray-700"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action_name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"show"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Show this post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Edit this post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edit_post_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;hr&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Done!
&lt;/h2&gt;

&lt;p&gt;And with that, we have infinite scroll using Ruby on Rails and Turbo 8. No additional JavaScript/Stimulus.js is necessary. I think this is a game changer and I hope this tip helps you code your Rails app a little faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/justalever/digging-into-view-transitions-with-turbo-8-and-rails-3plk-temp-slug-1484289"&gt;Digging into View Transitions with Turbo 8 and Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webcrunch.com/posts/turbo-morphing-ruby-on-rails"&gt;Digging into Turbo 8's Morphing Feature in Ruby on Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/justalever/how-to-add-live-reload-to-your-rails-application-36id-temp-slug-3994798"&gt;How to Add Live Reload to Your Rails Application&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Add Geolocation to Search with Rails and Stimulus.js</title>
      <dc:creator>Andy Leverenz</dc:creator>
      <pubDate>Mon, 19 Feb 2024 18:33:52 +0000</pubDate>
      <link>https://forem.com/justalever/add-geolocation-to-search-with-rails-and-stimulusjs-13mh</link>
      <guid>https://forem.com/justalever/add-geolocation-to-search-with-rails-and-stimulusjs-13mh</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/GkqZoIK1HhY"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In this tutorial, I'll show you how to add Geolocation support to a search form using Ruby on Rails and Stimulus.js&lt;/p&gt;

&lt;p&gt;By integrating geolocation capabilities, users can search for locations based on their current position, which saves time and effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  New up a Rails application
&lt;/h2&gt;

&lt;p&gt;To kick things off, let’s create a new app I'll call &lt;code&gt;geodemo&lt;/code&gt;. I prefer Tailwind for my CSS and esbuild for JavaScript. We'll leverage Stimulus.js for this tutorial so you can choose whatever JavaScript engine you like as it adapts to each gracefully.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new geodemo &lt;span class="nt"&gt;-c&lt;/span&gt; tailwind &lt;span class="nt"&gt;-j&lt;/span&gt; esbuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add Dependencies
&lt;/h3&gt;

&lt;p&gt;Let’s install some dependencies. A lovely gem called &lt;a href="https://github.com/alexreisner/geocoder"&gt;Geocoder&lt;/a&gt; will help us find locations based on an address or set of latitude and longitude lookups. It’s super powerful!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add geocoder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add a Search Controller
&lt;/h3&gt;

&lt;p&gt;For this tutorial, we’ll make a global search rails controller. I won’t get into the weeds of &lt;em&gt;what&lt;/em&gt; we would search but you could use something like &lt;a href="https://github.com/Casecommons/pg_search"&gt;pg_search&lt;/a&gt; to build a query from a model you need to return results of.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g controller search index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’ll put a basic form in &lt;code&gt;app/views/search/index.html.erb&lt;/code&gt; coming up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add a SearchLocationController
&lt;/h3&gt;

&lt;p&gt;We need a view and request cycle to make this all work so let’s generate a &lt;code&gt;SearchLocation&lt;/code&gt; controller with Rails to handle the &lt;code&gt;POST&lt;/code&gt; request. For the sake of the guide, I'll add a route for the index action to display the form we'll leverage coming up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g controller search_location create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should generate the controller in &lt;code&gt;app/controllers&lt;/code&gt; a couple of views and some default routes. Let's amend the routes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'search/index'&lt;/span&gt;
  &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'search_location'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"search_location#create"&lt;/span&gt;

  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"up"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"rails/health#show"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :rails_health_check&lt;/span&gt;

  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="s2"&gt;"search#index"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can boot up your server and hopefully see the new root path active at &lt;code&gt;localhost:3000&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add the search form
&lt;/h3&gt;

&lt;p&gt;Inside &lt;code&gt;app/views/search/index.html.erb&lt;/code&gt; I've added this erb code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&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;"max-w-xl mx-auto my-16"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-3xl mb-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Search by location&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"geolocation"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_tag&lt;/span&gt; &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;search_index_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :get&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;text_field_tag&lt;/span&gt; &lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"px-6 py-3 rounded border border-gray-300/80 focus:ring-4 focus:ring-gray-200/60 focus:outline-none w-full rounded-full text-lg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"Search state or city"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;geolocation_target: &lt;/span&gt;&lt;span class="s1"&gt;'input'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"absolute top-1.5 right-2 bg-white rounded-full px-5 py-2 border border-gray-300/80 shadow-sm bg-gray-50/50 hover:bg-gray-50"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Search&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"click-&amp;gt;geolocation#handleClick"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"underline font-medium mt-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Find my location&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code scaffolds a basic search form that would perform a GET request to our search controller index action.&lt;/p&gt;

&lt;p&gt;If we had real search records in our app, we could return real data in some fashion after submission.&lt;/p&gt;

&lt;p&gt;I’ll leave that for you to sort out since this tutorial is more about geolocation.&lt;/p&gt;

&lt;p&gt;If you look closely, you’ll see some data attributes on our HTML code.&lt;/p&gt;

&lt;p&gt;These will be how Stimulus.js knows about our form fields and actions.&lt;/p&gt;

&lt;p&gt;The button “Find my location” has an action that our Stimulus.js controller I’ll call &lt;code&gt;geolocation_controller.js&lt;/code&gt; will work with. Let’s make that next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up the Geolocation Stimulus.js Controller
&lt;/h3&gt;

&lt;p&gt;Let's create a Stimulus controller to handle geolocation functionality.&lt;/p&gt;

&lt;p&gt;Create a new file named &lt;code&gt;geolocation_controller.js&lt;/code&gt; in the &lt;code&gt;app/javascript/controllers&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;You can also run this handy rails command to generate one fast.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g stimulus geolocation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the new file, I’ve added the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nf"&gt;handleClick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geolocation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleSuccess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleError&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Geolocation is not supported by this browser.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;handleSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;longitude&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendLocationToRails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;handleError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Geolocation error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;sendLocationToRails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;csrfToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meta[name="csrf-token"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;

    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/search_location&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-CSRF-Token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;csrfToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;longitude&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP error! Status: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error communicating with Rails:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="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;Here’s a quick outline of what this code is responsible for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Targets&lt;/strong&gt; : It declares a static property &lt;code&gt;targets&lt;/code&gt;, specifying the targets that the controller will interact with. In this case, it specifies an input target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;handleClick Method&lt;/strong&gt; : This method is triggered when a user clicks on an element associated with this controller. It checks if the browser supports geolocation. If it does, it calls &lt;code&gt;getCurrentPosition&lt;/code&gt; method of the &lt;code&gt;navigator.geolocation&lt;/code&gt; object, passing two callback functions for success and error handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;handleSuccess Method&lt;/strong&gt; : This method is invoked upon successfully retrieving the user's geolocation. It extracts the latitude and longitude from the position object and calls &lt;code&gt;sendLocationToRails&lt;/code&gt; method with these coordinates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;handleError Method&lt;/strong&gt; : This method handles errors encountered during geolocation retrieval by logging an error message to the console.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sendLocationToRails Method&lt;/strong&gt; : This method sends the user's location (latitude and longitude) to a Rails backend endpoint using a fetch request. It retrieves the CSRF token from the meta tags, includes it in the request headers, and sends the location data as JSON in the request body. It then processes the response, updating the input field value based on the data received (either a zip code or a city name). Any errors during this process are caught and logged to the console.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Handle Geolocation Response in Rails Controller
&lt;/h3&gt;

&lt;p&gt;In your Rails controller (&lt;code&gt;search_controller.rb&lt;/code&gt;), implement the logic to handle the geolocation data received from the frontend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SearchLocationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="c1"&gt;# Access latitude and longitude parameters sent from frontend&lt;/span&gt;
    &lt;span class="n"&gt;latitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:latitude&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;longitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:longitude&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Implement your logic to determine a location based on coordinates&lt;/span&gt;
    &lt;span class="n"&gt;location_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Geocoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="n"&gt;location_data&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;data&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code leverages the Geocoder gem we added before and returns JSON based on the values passed from the front end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing it out
&lt;/h3&gt;

&lt;p&gt;If all goes well, we should be able to click the “Find my location” link and propagate the input field with either a postal code or city, depending on what returns. This is a &lt;em&gt;one-click&lt;/em&gt; option to make it easier on your users who would instead give their location instead of typing one in the form manually.&lt;/p&gt;

&lt;p&gt;We extract the latitude and longitude coordinates from the &lt;code&gt;navigator.geolocation&lt;/code&gt; browser API. Those coordinates get submit via JavaScript as a POST request to our &lt;code&gt;search_location#create&lt;/code&gt; method in the &lt;code&gt;SearchLocationController&lt;/code&gt; , which then performs the query to decode into a US state. Finally, that response returns to the front end and dynamically sets the input's value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Happy dance, it works!
&lt;/h3&gt;

&lt;p&gt;In this tutorial, we've successfully integrated geolocation support into a search form in a Rails application using Stimulus.js.&lt;/p&gt;

&lt;p&gt;The lazy users (we all know we are one) can now conveniently search for locations based on their current position in one click.&lt;/p&gt;

&lt;p&gt;Now there is plenty of room for improvement here, but I hope I’ve guided you in a good direction to build something useful.&lt;/p&gt;

&lt;p&gt;Experiment with additional features, like error handling and showing user feedback such as loading states and more.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>Digging into View Transitions with Turbo 8 and Rails</title>
      <dc:creator>Andy Leverenz</dc:creator>
      <pubDate>Mon, 12 Feb 2024 17:03:25 +0000</pubDate>
      <link>https://forem.com/justalever/digging-into-view-transitions-with-turbo-8-and-rails-31p2</link>
      <guid>https://forem.com/justalever/digging-into-view-transitions-with-turbo-8-and-rails-31p2</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/gdePV0Beihg"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This is a super quick tutorial to show you the new ViewTransition API that ships with Turbo 8, released recently.&lt;/p&gt;

&lt;p&gt;View Transitions are a neat way to add more fluidity between views in your app. With Rails and Turbo 8, adding this effect is pretty straightforward. The most challenging obstacle is understanding the CSS side of the equation.&lt;/p&gt;

&lt;p&gt;Let’s dive in and start experimenting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a new rails app
&lt;/h3&gt;

&lt;p&gt;I’ll use a basic Rails app with esbuild for JavaScript. We won’t use JavaScript in this tutorial, so feel free to use the defaults.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new turbo_view_transitions &lt;span class="nt"&gt;-j&lt;/span&gt; esbuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To save time, I’ll add the &lt;a href="https://github.com/rails/tailwindcss-rails"&gt;tailwind-rails gem&lt;/a&gt; to get scaffold UI out of the box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add tailwindcss-rails
rails tailwindcss:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this out of the way we can generate a new &lt;code&gt;Post&lt;/code&gt; scaffold with some demo columns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g scaffold Post title:string body:text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update your root route to &lt;code&gt;posts#index&lt;/code&gt;&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="c1"&gt;# config/routes.rb&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;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:posts&lt;/span&gt;  
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"up"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"rails/health#show"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :rails_health_check&lt;/span&gt;
  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="s2"&gt;"posts#index"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Modifying the Rails app main layout
&lt;/h3&gt;

&lt;p&gt;To use view transitions with Turbo 8, add a new meta tag to your layout’s &lt;code&gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&lt;/code&gt; tags. &lt;code&gt;&amp;lt;meta name="view-transition" content="same-origin" /&amp;gt;&lt;/code&gt;&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="c"&gt;&amp;lt;!-- app/views/layouts/application.html.erb--&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;TurboViewTransitions&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width,initial-scale=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"view-transition"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"same-origin"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;csrf_meta_tags&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;csp_meta_tag&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;tailwind&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;inter-font&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;data-turbo-track&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;reload&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;data-turbo-track&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;reload&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;javascript_include_tag&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;data-turbo-track&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;reload&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;type:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container mx-auto mt-10 px-5 flex"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;yield&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s my modified layout. The main change is the new meta tag &lt;code&gt;&amp;lt;meta name="view-transition" content="same-origin" /&amp;gt;&lt;/code&gt;. I also changed the &lt;code&gt;main&lt;/code&gt; tag to use a smaller margin at the top of the page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Modifying the posts UI
&lt;/h3&gt;

&lt;p&gt;In the &lt;code&gt;app/views/posts/index.html.erb&lt;/code&gt; I added some new markup to help make this demo more visually appealing.&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;"w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt; &lt;span class="na"&gt;notice.present&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;notice&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"grid grid-cols-12 gap-16"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col-span-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-between items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Posts&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;New&lt;/span&gt; &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;new_post_path&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;rounded-lg&lt;/span&gt; &lt;span class="na"&gt;py-3&lt;/span&gt; &lt;span class="na"&gt;px-5&lt;/span&gt; &lt;span class="na"&gt;bg-blue-600&lt;/span&gt; &lt;span class="na"&gt;text-white&lt;/span&gt; &lt;span class="na"&gt;block&lt;/span&gt; &lt;span class="na"&gt;font-medium&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"posts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;aside&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col-span-4 bg-blue-600 rounded-lg text-white p-10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-semibold text-3xl sidebar-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Sidebar title&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This is my sidebar&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consider this a basic blog with a sidebar. We’ll leverage view transitions to make it snazzy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add custom view transition CSS
&lt;/h2&gt;

&lt;p&gt;Now for the exciting part. Let's add some view transition effects starting from simple to more complex.&lt;/p&gt;

&lt;h4&gt;
  
  
  Simple global transitions (easy)
&lt;/h4&gt;

&lt;p&gt;You may be interested in a slight animation on every view transition. To do that, you could add a couple of lines of CSS to &lt;code&gt;app/assets/stylesheets/application.tailwind.css&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;/* Global slight fade */&lt;/span&gt;
&lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
&lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation-duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5s&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;Target the root of the page directly and add a global transition.&lt;/p&gt;

&lt;h3&gt;
  
  
  How about different types of animations?
&lt;/h3&gt;

&lt;p&gt;Want to get a little more sophisticated? Try these styles that offer some slide and fade effects all in one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;fade-in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;fade-out&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;slide-from-right&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;60px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;slide-up&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;130px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;slide-to-left&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-30px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Global but more fancy */&lt;/span&gt;
&lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;fade-out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;300ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-to-left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;210ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;90ms&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;fade-in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;300ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-from-right&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;h3&gt;
  
  
  Singling out elements and combining transitions
&lt;/h3&gt;

&lt;p&gt;Say you want to target independent elements directly but also have global transition effects. That’s possible. Here’s my code thus far.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;fade-in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;fade-out&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;slide-from-right&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;60px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;slide-up&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;130px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;slide-to-left&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-30px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Global but more fancy */&lt;/span&gt;
&lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;fade-out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;300ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-to-left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;210ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;90ms&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;fade-in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;300ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-from-right&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Sidebar */&lt;/span&gt;
&lt;span class="nt"&gt;aside&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-turbo-visit-direction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"forward"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;sidebar&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;fade-in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;210ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-up&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;With turbo, we’re able to target directions. And in doing so, you can even tie into either the &lt;code&gt;::view-transition-new&lt;/code&gt; or &lt;code&gt;::view-transition-old&lt;/code&gt; states of the transition. With some gnarly CSS, you can target elements independently of one another and perform some wacky animations.&lt;/p&gt;

&lt;p&gt;Notice how, much like CSS Grid, you can name “areas” to define layouts. In this specific case, you can use &lt;code&gt;view-transition-name&lt;/code&gt; to name an element in the dom for use in other targeted CSS.&lt;/p&gt;

&lt;p&gt;So I used&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Sidebar */&lt;/span&gt;
&lt;span class="nt"&gt;aside&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This targets the &lt;code&gt;aside&lt;/code&gt; element in &lt;code&gt;app/views/posts/index.html.erb&lt;/code&gt; and gives us a way to define a new transitional style based on the turbo directions. Here's the example from before with that in action. Notice how our named view transition is "sidebar.”&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-turbo-visit-direction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"forward"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;sidebar&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;fade-in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;210ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-up&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hooking into Turbo
&lt;/h2&gt;

&lt;p&gt;Turbo adds a &lt;code&gt;data-turbo-visit-direction&lt;/code&gt; attribute to the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element to indicate the direction of the transition. The attribute can have one of the following values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;forward&lt;/code&gt; in advance visits.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;back&lt;/code&gt; in restoration visits.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;none&lt;/code&gt; in replace visits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Depending on the request, if you view the source when clicking a link or button, the HTML element will quickly update with the new data attributes. As a result, your CSS can capture the &lt;code&gt;forward,&lt;/code&gt; &lt;code&gt;backward,&lt;/code&gt; and &lt;code&gt;none&lt;/code&gt; directions to manipulate the elements on the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser Compatibility
&lt;/h2&gt;

&lt;p&gt;Only Chrome and Microsoft Edge are currently supported, but I anticipate Safari and Firefox to follow soon. Keep this in mind as you add this to your apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the real-world use case for view transitions?
&lt;/h2&gt;

&lt;p&gt;Having toyed with View Transitions, these will be extremely popular for folks leveraging Rails to build mobile apps. It allows you to do less with more native versus non-native apps. We see Turbo in use on the Basecamp team's apps already today, and it's about to get more familiar with other apps.&lt;/p&gt;

&lt;p&gt;This is all not to say web apps or progressive web apps shouldn't leverage this feature. It will be overused to some degree. I remember when the trend of animating everything on a marketing website was popular. You couldn't scroll without something moving in or out of view. I worry we're in for that again...&lt;/p&gt;

&lt;h2&gt;
  
  
  We’ve only scratched the surface
&lt;/h2&gt;

&lt;p&gt;This tutorial is super basic, but I hope it sheds some light on what’s possible with a little dash of CSS and tools you’re already using with Ruby on Rails. Among many new features, Turbo 8 brings a dash of life with View Transitions. Rails apps are about to get more pleasing, and I’m excited and here for it!&lt;/p&gt;

&lt;h4&gt;
  
  
  Links
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://turbo.hotwired.dev/handbook/drive#view-transitions"&gt;Hotwired Dev - Turbo View Transitions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API"&gt;MDN - View Transitions API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/web-platform/view-transitions"&gt;Chrome Developer Article on View Transitions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hotwired/turbo"&gt;Turbo on Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hellorails.io"&gt;Learn Ruby on Rails&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>turbo</category>
    </item>
    <item>
      <title>Create a Simple and Responsive Tailwind Navbar with Dropdown</title>
      <dc:creator>Andy Leverenz</dc:creator>
      <pubDate>Mon, 05 Feb 2024 19:45:11 +0000</pubDate>
      <link>https://forem.com/justalever/create-a-simple-and-responsive-tailwind-navbar-with-dropdown-6bl</link>
      <guid>https://forem.com/justalever/create-a-simple-and-responsive-tailwind-navbar-with-dropdown-6bl</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/h_xmLBxS5SA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Creating a responsive and visually appealing navigation bar is crucial for any website, and Tailwind CSS offers a streamlined, utility-first approach to achieve this.&lt;/p&gt;

&lt;p&gt;This guide will walk you through constructing a simple Tailwind navbar component with a dropdown menu. We’ll also ensure the navigation is adaptable across all devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Tailwind CSS for Responsive Navbars?
&lt;/h2&gt;

&lt;p&gt;Tailwind CSS simplifies building responsive navbars by providing utility classes that can be applied directly in your HTML, eliminating the need for extensive custom CSS. Whether you aim to implement a Tailwind navbar with dropdown functionalities or just a navigation component, Tailwind has you covered.&lt;/p&gt;

&lt;h4&gt;
  
  
  Table of Contents
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Step 1: Add the HTML for a Simple Tailwind Navbar&lt;/li&gt;
&lt;li&gt;Step 2: Responsive Design with Tailwind CSS&lt;/li&gt;
&lt;li&gt;Step 3: Toggling the Dropdown Menu&lt;/li&gt;
&lt;li&gt;Step 4: Toggling the Tailwind Navbar mobile menu&lt;/li&gt;
&lt;li&gt;Step 5: Customizing and Testing Your Tailwind Navbar&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Prerequisites
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Basic understanding of &lt;code&gt;HTML&lt;/code&gt; and &lt;code&gt;CSS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tailwind CSS integrated into your project&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Add the HTML for a Simple Tailwind Navbar
&lt;/h3&gt;

&lt;p&gt;The foundation of our Tailwind navbar component begins with its HTML structure. Here's a straightforward layout to start with:&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;nav&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-800 text-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container mx-auto px-4 md:flex items-center gap-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Logo --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center justify-between md:w-auto w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center py-5 px-2 text-white flex-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Webcrunch&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!-- Mobile Menu Button --&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"md:hidden flex items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mobile-menu-button"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;bars-3-bottom-left&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"1.5"&lt;/span&gt; &lt;span class="na"&gt;stroke-linecap=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/g&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Primary Navigation --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hidden md:flex md:flex-row flex-col items-center justify-start md:space-x-1 navigation-menu pb-3 md:pb-0 navigation-menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 block"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Home&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 block"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;About&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!-- Dropdown Menu --&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"dropdown-toggle py-2 px-3 hover:bg-gray-700 flex items-center gap-2 rounded"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pointer-events-none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Services&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-3 h-3 pointer-events-none"&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;chevron-down&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M19.5 8.25l-7.5 7.5-7.5-7.5"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"1.5"&lt;/span&gt; &lt;span class="na"&gt;stroke-linecap=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/g&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"dropdown-menu absolute hidden bg-gray-700 text-white rounded-b-lg pb-2 w-48"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"block px-6 py-2 hover:bg-gray-800"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Web Design&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"block px-6 py-2 hover:bg-gray-800"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Development&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"block px-6 py-2 hover:bg-gray-800"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;SEO&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 block"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Contact&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Responsive Design with Tailwind CSS
&lt;/h3&gt;

&lt;p&gt;To ensure our Tailwind navbar is responsive, we utilize Tailwind's built-in classes like &lt;code&gt;md:flex&lt;/code&gt; for medium screens and &lt;code&gt;hidden&lt;/code&gt; to toggle visibility. These utilities allow the navbar to adapt seamlessly across different devices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Toggling the Dropdown Menu
&lt;/h3&gt;

&lt;p&gt;For the dropdown menu within our Tailwind navbar component, we can add a bit of JavaScript to handle the interaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DOMContentLoaded&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Select all dropdown toggle buttons&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dropdownToggles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.dropdown-toggle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;dropdownToggles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Find the next sibling element which is the dropdown menu&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dropdownMenu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextElementSibling&lt;/span&gt;

      &lt;span class="c1"&gt;// Toggle the 'hidden' class to show or hide the dropdown menu&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dropdownMenu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&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="c1"&gt;// Hide any open dropdown menus before showing the new one&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.dropdown-menu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="nx"&gt;dropdownMenu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;dropdownMenu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Optional: Clicking outside of an open dropdown menu closes it&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.dropdown-toggle&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.dropdown-menu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;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;h3&gt;
  
  
  Step 4: Toggling the Tailwind Navbar mobile menu
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Mobile menu toggle&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mobileMenuButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.mobile-menu-button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mobileMenu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.navigation-menu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;mobileMenuButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;mobileMenu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On mobile screens, we hide the menu from view and display a hamburger menu icon. This icon is tapable on smaller screens. The javascript targets both the mobile menu and th&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Customizing and Testing Your Tailwind Navbar
&lt;/h3&gt;

&lt;p&gt;Tailwind CSS makes building a navigation component a breeze and offers endless customization options to ensure your navbar aligns perfectly with your brand's aesthetics. Let's dive deeper into how you can personalize your navbar with features like sticky navigation and dark mode support and why testing across different devices is crucial for user experience.&lt;/p&gt;

&lt;h4&gt;
  
  
  Customizing the Tailwind Navbar
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Color Schemes&lt;/strong&gt; : Tailwind provides various color utilities. You can change your navbar's background, text, and hover states to match your brand colors. For instance, use &lt;code&gt;bg-blue-500&lt;/code&gt; for a blue background or &lt;code&gt;text-white&lt;/code&gt; for white text. Don't forget to apply hover effects with classes like &lt;code&gt;hover:bg-blue-700&lt;/code&gt; for interactive elements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spacing and Layout&lt;/strong&gt; : Tailwind's spacing utilities allow you to adjust padding and margin for the perfect layout. Use classes like &lt;code&gt;px-4&lt;/code&gt; (padding on the x-axis) or &lt;code&gt;my-2&lt;/code&gt; (margin on the y-axis) to space elements within your navbar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typography&lt;/strong&gt; : Customize the text size, weight, and color using Tailwind's typography classes. For example, &lt;code&gt;text-lg&lt;/code&gt; for larger text or &lt;code&gt;font-bold&lt;/code&gt; for bold text can make your navbar items stand out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Implementing a Tailwind Sticky Nav
&lt;/h4&gt;

&lt;p&gt;To make your navbar sticky, meaning it stays at the top of the viewport as the user scrolls, add the &lt;code&gt;sticky&lt;/code&gt; and &lt;code&gt;top-0&lt;/code&gt; classes to your &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; element. This ensures that your navigation is always accessible, regardless of where the user is on the page:&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;nav&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sticky top-0 bg-gray-800 text-white z-50"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;z-50&lt;/code&gt; class ensures your navbar stays above other content. Adjust the &lt;code&gt;z-index&lt;/code&gt; as needed based on your page's elements.&lt;/p&gt;

&lt;h4&gt;
  
  
  Adding Dark Mode Support
&lt;/h4&gt;

&lt;p&gt;Tailwind CSS v2.0 and above supports dark mode out of the box, making it easy for users who prefer a darker interface to adapt your navbar design. Use the &lt;code&gt;dark:&lt;/code&gt; prefix to specify different styles for dark mode:&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;nav&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-800 text-white dark:bg-black dark:text-gray-200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, the navbar will have a black background and lighter text in dark mode. Refer to Tailwind's documentation for proper dark mode configuration. By default, you can define dark mode based on a global body class name or support device-enabled dark mode.&lt;/p&gt;

&lt;h4&gt;
  
  
  Testing Across Devices
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Responsiveness&lt;/strong&gt;: Use browser dev tools to simulate various screen sizes and ensure your navbar looks good on all devices. Pay special attention to breakpoints where your layout changes, such as when the mobile menu button appears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactivity&lt;/strong&gt;: Test interactive elements like dropdown menus and links to ensure they work as expected on touch devices and with mouse and keyboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Ensure your navbar performs well, especially when implementing features like sticky positioning or dynamic dark mode switching, which can impact rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility&lt;/strong&gt;: Test for accessibility, ensuring your navbar is navigable using keyboard shortcuts and screen readers. This includes proper ARIA labels for interactive elements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Conclusion
&lt;/h4&gt;

&lt;p&gt;With Tailwind CSS, creating a simple, responsive navbar component with a dropdown menu becomes a straightforward process. By leveraging Tailwind's utility classes, you can efficiently create a navigation bar that's both stylish and functional across all screen sizes. This guide has shown you how to integrate essential features into your Tailwind navbar, enhancing your website's navigation experience for users on any device.&lt;/p&gt;

&lt;h3&gt;
  
  
  Continue reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/justalever/javascript-mouse-tracking-glow-effect-with-tailwind-css-5bno-temp-slug-594769"&gt;JavaScript mouse-tracking Glow Effect with Tailwind CSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webcrunch.com/posts/tailwind-css-button-components"&gt;Tailwind CSS Button Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webcrunch.com/posts/code-a-mega-menu-with-tailwind-css"&gt;Code a mega menu with Tailwind CSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webcrunch.com/posts/re-create-airbnbs-home-page-with-tailwind-css"&gt;Re-create Airbnb's Home Page with Tailwind CSS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>css</category>
      <category>tutorial</category>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>How to Add Live Reload to Your Rails Application</title>
      <dc:creator>Andy Leverenz</dc:creator>
      <pubDate>Sun, 28 Jan 2024 17:09:29 +0000</pubDate>
      <link>https://forem.com/justalever/how-to-add-live-reload-to-your-rails-application-2jc</link>
      <guid>https://forem.com/justalever/how-to-add-live-reload-to-your-rails-application-2jc</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/Pf3Kpbb4ZHg"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Live reload is an incredible tool for developers looking to boost their productivity. In this tutorial, I will walk you through implementing live reload in your Ruby on Rails application using esbuild.&lt;/p&gt;

&lt;p&gt;Implementing live reload can significantly streamline your development workflow, saving you valuable time and effort. You can instantly see the results by automatically refreshing your app whenever you make changes without manually reloading the page.&lt;/p&gt;

&lt;p&gt;To achieve this, we will leverage the power of esbuild, a cutting-edge technology for fast and efficient JavaScript bundling. By combining esbuild with Ruby on Rails, we can create a seamless live reload experience that will streamline your development process.&lt;/p&gt;




&lt;h3&gt;
  
  
  Table of Contents
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Setting Up Your Rails Project&lt;/li&gt;
&lt;li&gt;Installing Necessary Gems and Dependencies&lt;/li&gt;
&lt;li&gt;Configuring Esbuild&lt;/li&gt;
&lt;li&gt;Creating the Live Reload Script&lt;/li&gt;
&lt;li&gt;Putting It All Together&lt;/li&gt;
&lt;li&gt;Live reload alternatives for Ruby on Rails&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;li&gt;Continue learning&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;Live Reload automates the refresh process of your web page whenever you make changes to your code. This feature not only saves time but also enhances your overall development experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up Your Rails Project
&lt;/h3&gt;

&lt;p&gt;Start by creating a new Rails project with Tailwind CSS and esbuild:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new livereload &lt;span class="nt"&gt;-c&lt;/span&gt; tailwind &lt;span class="nt"&gt;-j&lt;/span&gt; esbuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Installing Necessary Gems and Dependencies
&lt;/h3&gt;

&lt;p&gt;First, ensure you have the &lt;code&gt;jsbundling-rails&lt;/code&gt; gem installed. If not, add it to your Gemfile and run &lt;code&gt;bundle install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, set up esbuild with Rails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./bin/bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rails javascript:install:esbuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the &lt;code&gt;chokidar&lt;/code&gt; package for file watching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add chokidar &lt;span class="nt"&gt;-D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuring esbuild
&lt;/h3&gt;

&lt;p&gt;Create and customize an &lt;code&gt;esbuild.config.js&lt;/code&gt; file in your project:&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="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;esbuild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;esbuild&lt;/span&gt; &lt;span class="o"&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="s2"&gt;esbuild&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chokidar&lt;/span&gt; &lt;span class="o"&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="s2"&gt;chokidar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&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="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&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="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isProduction&lt;/span&gt; &lt;span class="o"&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;RAILS_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watchMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&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="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--reload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reloadMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--reload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5678&lt;/span&gt;

&lt;span class="c1"&gt;// Esbuild configuration&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;esbuildConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/javascript&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;application.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
  &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;minify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isProduction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isProduction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/assets/builds&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="c1"&gt;// Start the esbuild build process&lt;/span&gt;
&lt;span class="nx"&gt;esbuild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;esbuildConfig&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// Set up a simple HTTP server for live reload with CORS headers&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clients&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="c1"&gt;// Set CORS headers&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Origin&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;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Methods&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;GET, OPTIONS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Headers&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;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/event-stream&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;Cache-Control&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;no-cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keep-alive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;close&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5678&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Watch for file changes &lt;/span&gt;
&lt;span class="nx"&gt;chokidar&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/javascript/**/*&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;app/views/**/*&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;app/assets/stylesheets/**/*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;esbuild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;esbuildConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data: reload&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Live reload server running on http://localhost:5678&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration sets up &lt;code&gt;esbuild&lt;/code&gt; and a simple HTTP server for the live reload functionality.&lt;/p&gt;

&lt;p&gt;Here's a simplified explanation of what the code in &lt;code&gt;esbuild.config.js&lt;/code&gt; is doing, hopefully, broken down into more straightforward terms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Setting Up Tools and Checking the Environment&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;The code starts by loading some necessary tools: &lt;code&gt;esbuild&lt;/code&gt;, &lt;code&gt;chokidar&lt;/code&gt;, and &lt;code&gt;http&lt;/code&gt;. &lt;code&gt;esbuild&lt;/code&gt; helps in bundling and optimizing JavaScript files, &lt;code&gt;chokidar&lt;/code&gt; watches for file changes, and &lt;code&gt;http&lt;/code&gt; is used to create a server.&lt;/li&gt;
&lt;li&gt;It checks if the Rails app is in production mode. This helps the code decide whether to optimize files for faster performance or not.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuring Esbuild&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;A configuration is set for &lt;code&gt;esbuild&lt;/code&gt;. This includes where to find the JavaScript files, whether to bundle them together and where to save them. It's like giving &lt;code&gt;esbuild&lt;/code&gt; a map and a to-do list.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting the Build Process&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;esbuild&lt;/code&gt; starts its job using the configuration provided. If something goes wrong, it stops the process to avoid more issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creating a Mini Server for Live Reload&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;The code sets up a small server. This server's job is to send messages to the web browser to refresh the page whenever there are changes.&lt;/li&gt;
&lt;li&gt;It makes sure that any browser is allowed to connect to this server (that's what CORS headers do).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Listening for Changes&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;chokidar&lt;/code&gt; starts watching files in your JavaScript, views, and stylesheets directories. Whenever there's a change in these files, it tells &lt;code&gt;esbuild&lt;/code&gt; to rebuild them.&lt;/li&gt;
&lt;li&gt;After rebuilding, it notifies all connected browsers to refresh the page. This means you see the changes in your web app immediately ✨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running the Server&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;Finally, the server starts, and the code tells you that it's running and ready to reload your pages automatically on &lt;code&gt;http://localhost:5678&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Creating the Live Reload Script
&lt;/h3&gt;

&lt;p&gt;Now, add a &lt;code&gt;live_reload.js&lt;/code&gt; file to &lt;code&gt;app/javascript&lt;/code&gt; and import it in &lt;code&gt;application.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/live_reload.js&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;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="s2"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:5678&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reload&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script listens for the "reload" event cast from our esbuild configuration and refreshes the page. We created a new &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource"&gt;EventSource&lt;/a&gt; for the server created by the &lt;code&gt;build&lt;/code&gt; configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Putting It All Together
&lt;/h3&gt;

&lt;p&gt;With these configurations, your Rails app is now set up for live reloading. You can start your Rails server and begin coding. The browser automatically reloads whenever you change your JavaScript, stylesheets, or views. This ultimately saves you time and sanity during development.&lt;/p&gt;

&lt;h3&gt;
  
  
  Live reload Alternatives for Ruby on Rails
&lt;/h3&gt;

&lt;p&gt;If you’re looking for alternatives to the esbuild-driven method I introduced in this tutorial, there are plenty of other options with Rails. Some options depend on specific technologies or front-end frameworks but accomplish similar goals.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Guard LiveReload&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Description: Guard LiveReload automatically refreshes your browser when 'view' files are modified.&lt;/li&gt;
&lt;li&gt;Link: &lt;a href="https://github.com/guard/guard-livereload"&gt;Guard LiveReload on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rack::LiveReload&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Description: A Rack middleware that inserts the LiveReload script into your app's responses. Works well with apps not using Webpacker.&lt;/li&gt;
&lt;li&gt;Link: &lt;a href="https://github.com/jaredmdobson/rack-livereload"&gt;Rack::LiveReload on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webpacker&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Description: Though primarily for managing JavaScript, Webpacker can be configured for hot module replacement, similar to live reloading.&lt;/li&gt;
&lt;li&gt;Link: &lt;a href="https://github.com/rails/webpacker"&gt;Webpacker on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BrowserSync&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Description: Not specific to Rails, BrowserSync can synchronize file changes and interactions across multiple devices, ideal for responsive testing.&lt;/li&gt;
&lt;li&gt;Link: &lt;a href="https://browsersync.io/"&gt;BrowserSync&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Hot Loader&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Description: React Hot Loader enables hot module replacement for a smoother development experience if you use React with Rails.&lt;/li&gt;
&lt;li&gt;Link: &lt;a href="https://github.com/gaearon/react-hot-loader"&gt;React Hot Loader on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite Ruby&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Description: Vite Ruby integrates Vite.js with Ruby on Rails, offering a fast front-end build tool with features like hot module replacement.&lt;/li&gt;
&lt;li&gt;Link: &lt;a href="https://github.com/ElMassimo/vite_ruby"&gt;Vite Ruby on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Live Reload is an awesome tool for enhancing your Rails development workflow. It reduces the time spent on manual refreshes, allowing you to focus more on development. The result is more efficient coding and better development of apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Continue learning
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://%5Bhttps://webcrunch.com/posts/bun-with-ruby-on-rails%5D(https://webcrunch.com/posts/bun-with-ruby-on-rails)"&gt;How to use bun with Ruby on Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webcrunch.com/posts/turbo-morphing-ruby-on-rails"&gt;Digging into Turbo 8's Morphing Feature in Ruby on Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webcrunch.com/posts/digging-into-turbo-with-ruby-on-rails-7"&gt;Digging into Turbo with Ruby on Rails 7&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>JavaScript mouse-tracking Glow Effect with Tailwind CSS</title>
      <dc:creator>Andy Leverenz</dc:creator>
      <pubDate>Tue, 16 Jan 2024 21:13:58 +0000</pubDate>
      <link>https://forem.com/justalever/javascript-mouse-tracking-glow-effect-with-tailwind-css-jhg</link>
      <guid>https://forem.com/justalever/javascript-mouse-tracking-glow-effect-with-tailwind-css-jhg</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/2Cp9ECsQZ_Y"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In this Tailwind CSS tutorial, we'll explore how to add an interactive, mouse-tracking glow effect to web elements using just JavaScript and Tailwind CSS. This simple yet eye-catching effect will enhance the user experience on your webpage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic knowledge of HTML, CSS, and JavaScript.&lt;/li&gt;
&lt;li&gt;Tailwind CSS installed in your project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up a project
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://vitejs.dev/"&gt;I like to use vite.js for projects like this,&lt;/a&gt;but you could easily use something else. Vite has built-in support for many things, so it’s more efficient for me to test out new ideas and create tutorials such as this one.&lt;/p&gt;

&lt;p&gt;You can reference the &lt;a href="https://vitejs.dev/guide/#scaffolding-your-first-vite-project"&gt;docs&lt;/a&gt; or follow along as I create a new project. I used &lt;a href="https://bun.sh"&gt;bun&lt;/a&gt; for package management and vanilla JavaScript to keep things simple. Be sure to remove all the boilerplate Vite.js installs when scaffolding a project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx create-vite &lt;span class="c"&gt;# go through the wizard, then cd into your project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install Tailwind CSS
&lt;/h3&gt;

&lt;p&gt;Tailwind has some dependencies, so we’ll install those using bun. These are saved as dev dependencies because the &lt;code&gt;-d&lt;/code&gt; flag passed. You can confirm inside your &lt;code&gt;package.json&lt;/code&gt; file post-install.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add tailwindcss postcss autoprefixer @tailwindcss/typography &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we need to generate a tailwind configuration and postcss configuration file. This can be done with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx tailwindcss init &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the default Tailwind styles to your stylesheet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* style.css */&lt;/span&gt;

&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, you can boot your server running:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;I updated the default &lt;code&gt;tailwind.config.js&lt;/code&gt; file to the following for this tutorial. You will want to add directories of files possibly. &lt;a href="https://tailwindcss.com/docs/content-configuration"&gt;Read the docs&lt;/a&gt; for more information.&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="cm"&gt;/** @type {import('tailwindcss').Config} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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="s2"&gt;index.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;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="s2"&gt;@tailwindcss/typography&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Initial HTML Structure
&lt;/h3&gt;

&lt;p&gt;To make the glow effect more impactful, we can create a custom card that would benefit from the effect in the first place. I chose tropical fish.&lt;/p&gt;

&lt;p&gt;You can start by creating a basic HTML structure. Here, we have a &lt;code&gt;div&lt;/code&gt; containing a heading, paragraph, and button, wrapped in a parent &lt;code&gt;div&lt;/code&gt; with the class &lt;code&gt;glow-capture.&lt;/code&gt;&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="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Create a custom mouse tracking Glow Effect with Tailwind CSS&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;body&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-zinc-950 text-zinc-50 antialiased h-screen"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl mx-auto px-4 space-y-12 pb-32 pt-16 mt-10 mb-24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Fish 1 --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative glow-capture"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"group bg-zinc-900/50 border-4 border-white/5 rounded-2xl p-10 shadow-lg shadow-black/80 flex md:flex-row flex-col flex-wrap md:items-start items-center md:justify-between justify-center gap-6 backdrop-blur-md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 md:order-1 order-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"opacity-50"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Tropical fish&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl tracking-tighter mb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Banana Wrasse&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
             &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"md:prose-base prose-lg prose-zinc prose-invert text-opacity-90"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;The Banana Wrasse is known for its vibrant yellow color and elongated body, resembling a banana.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This active and playful fish thrives in tropical reef environments, often seen darting among corals and
              rocks.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

          &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-semibold px-6 bg-zinc-950/50 backdrop-blur-md py-3 rounded-xl text-white/90 border-2 border-white/10 mt-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Learn more&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"fish-1.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Banana Wrasse"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-sm rounded-full object-cover w-40 h-40 border-4 border-white/5 md:order-2 order-1 shadow-inner"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"glow-overlay"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"--glow-color: #7c3aed"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’ll start with one section that focuses on one fish for now. We can then duplicate the code and swap some colors, images, and content.&lt;/p&gt;

&lt;p&gt;You’ll see some custom classes, such as &lt;code&gt;glow-capture&lt;/code&gt; and &lt;code&gt;glow-overlay&lt;/code&gt;. These will come in handy when we define some JavaScript and extend Tailwind.&lt;/p&gt;

&lt;p&gt;Let’s tackle the CSS first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling with Tailwind CSS
&lt;/h2&gt;

&lt;p&gt;Add the following styles for the &lt;code&gt;.glow-overlay&lt;/code&gt; class. These styles create the base for our glow effect. You can leverage the &lt;code&gt;@apply&lt;/code&gt; logic with Tailwind CSS here. I kept this code traditional CSS for now.&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;mask&lt;/code&gt; and pass a &lt;code&gt;radial-gradient&lt;/code&gt; method. It's a fancy way to make gradients that aren't the simpler gradients you might be used to. Read more about &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mask"&gt;masks&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/radial-gradient"&gt;radial gradients&lt;/a&gt; on mdn.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.glow-overlay&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--glow-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;pointer-events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;user-select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--glow-opacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--glow-size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--glow-size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--glow-x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--glow-y&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--glow-color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;1%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400ms&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;will-change&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Extend Tailwind
&lt;/h2&gt;

&lt;p&gt;We need to extend Tailwind to have a new variant called &lt;code&gt;glow&lt;/code&gt;. Read more about &lt;a href="https://tailwindcss.com/docs/plugins#overview"&gt;plugins&lt;/a&gt; and &lt;a href="https://tailwindcss.com/docs/plugins#adding-variants"&gt;adding variants&lt;/a&gt; on the Tailwind CSS documentation.&lt;/p&gt;

&lt;p&gt;Here’s my revised &lt;code&gt;tailwind.config.js&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @type {import('tailwindcss').Config} */&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="o"&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="s2"&gt;tailwindcss/plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="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="s2"&gt;index.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;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="s2"&gt;@tailwindcss/typography&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;addVariant&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;addVariant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;glow&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;.glow-capture .glow-overlay &amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;glow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;color-mix(in srgb, var(--glow-color) calc(&amp;lt;alpha-value&amp;gt; * 100%), transparent)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code uses the &lt;code&gt;plugin&lt;/code&gt; extension of the Tailwind CSS library. We pass a new variant called &lt;code&gt;glow&lt;/code&gt;, which targets elements within the &lt;code&gt;glow-capture&lt;/code&gt; and &lt;code&gt;glow-overlay&lt;/code&gt; classes. The &lt;code&gt;&amp;amp;&lt;/code&gt; sign acts as a placeholder.&lt;/p&gt;

&lt;p&gt;Finally, with the new variant, we can extend the theme based on its usage. To do so, we use the color-mix property in CSS, leveraging the &lt;code&gt;—-glow-color&lt;/code&gt; CSS variable that gets defined in our HTML and later manipulated with JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add JavaScript for Mouse Tracking
&lt;/h2&gt;

&lt;p&gt;Write a JavaScript function to track the mouse movement and apply the glow effect. This code listens for mouse movement over the &lt;code&gt;.glow-capture&lt;/code&gt; element and updates CSS variables to move the glow effect.&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="cm"&gt;/* main.js */&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./style.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DOMContentLoaded&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;captures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.glow-capture&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;captures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;capture&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="c1"&gt;// Clone a child element. For example, we can clone the first child.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clonedChild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;cloneNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;overlay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.glow-overlay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Append the cloned child to the overlay.&lt;/span&gt;
    &lt;span class="nx"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clonedChild&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mousemove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageX&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetLeft&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetTop&lt;/span&gt;

      &lt;span class="nx"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--glow-x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--glow-y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--glow-opacity&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;1&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="c1"&gt;// Add mouseleave event to remove the glow effect&lt;/span&gt;
    &lt;span class="nx"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mouseleave&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--glow-opacity&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;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my Vite project, a &lt;code&gt;main.js&lt;/code&gt; file is the default entry point. I'm just chucking this code there. We do quite a few things in this file:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Listed first for the &lt;code&gt;DOMContentLoaded&lt;/code&gt; event and then proceed&lt;/li&gt;
&lt;li&gt;Find all elements with the &lt;code&gt;.glow-capture&lt;/code&gt; class&lt;/li&gt;
&lt;li&gt;Loop through each element with the &lt;code&gt;.glow-capture&lt;/code&gt; class and clone it.&lt;/li&gt;
&lt;li&gt;The overlay div added in our HTML gets a new child element which effectively doubles our original fish card.&lt;/li&gt;
&lt;li&gt;On the individual card, we listen for both the &lt;code&gt;mousemove&lt;/code&gt; and &lt;code&gt;mouseleave&lt;/code&gt; events, setting custom CSS styles based on the mouse &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; position. If you leave the card with your mouse, we remove opacity, so you can't see the "glow” effect.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Adding the new glow variant classes
&lt;/h3&gt;

&lt;p&gt;Here’s where it gets fun. Because we introduced the new Tailwind CSS variant called &lt;code&gt;glow&lt;/code&gt;, we can target elements within the card to change as we navigate the card with our mouse. You can apply the variants like any other Tailwind CSS variants.&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="c"&gt;&amp;lt;!-- Fish 1 --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative glow-capture"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"group bg-zinc-900/50 border-4 border-white/5 rounded-2xl p-10 shadow-lg shadow-black/80 flex md:flex-row flex-col flex-wrap md:items-start items-center md:justify-between justify-center gap-6 backdrop-blur-md glow glow:ring-1 glow:border-glow glow:ring-glow glow:bg-glow/[.15]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 md:order-1 order-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"opacity-50"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Tropical fish&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl tracking-tighter mb-3 glow:text-glow/[.15]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Banana Wrasse&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"md:prose-base prose-lg prose-zinc prose-invert text-opacity-90"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;The Banana Wrasse is known for its vibrant yellow color and elongated body, resembling a banana.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This active and playful fish thrives in tropical reef environments, often seen darting among corals and
              rocks.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

          &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
            &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-semibold px-6 bg-zinc-950/50 backdrop-blur-md py-3 rounded-xl text-white/90 border-2 border-white/10 mt-6 glow:ring-1 glow:border-glow glow:ring-glow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Learn
            more&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"fish-1.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Banana Wrasse"&lt;/span&gt;
          &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-sm rounded-full object-cover w-40 h-40 border-4 border-white/5 md:order-2 order-1 glow glow:ring-1 glow:border-glow/[.5] glow:ring-glow shadow-inner"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"glow-overlay"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"--glow-color: #7c3aed"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here I’ve applied it to the card, title, button, and fish image to produce a neat effect based on the &lt;code&gt;—glow-color&lt;/code&gt; variable passed to the div with the class &lt;code&gt;glow-overlay&lt;/code&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  Add more fish to the mix
&lt;/h2&gt;

&lt;p&gt;Here’s my entire HTML file:&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="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Create a Mouse Tracking Glow Effect with Tailwind CSS&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;body&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-zinc-950 text-zinc-50 antialiased h-screen"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl mx-auto px-4 space-y-12 pb-32 pt-16 mt-10 mb-24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Fish 1 --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative glow-capture"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"group bg-zinc-900/50 border-4 border-white/5 rounded-2xl p-10 shadow-lg shadow-black/80 flex md:flex-row flex-col flex-wrap md:items-start items-center md:justify-between justify-center gap-6 backdrop-blur-md glow glow:ring-1 glow:border-glow glow:ring-glow glow:bg-glow/[.15]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 md:order-1 order-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"opacity-50"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Tropical fish&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl tracking-tighter mb-3 glow:text-glow/[.15]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Banana Wrasse&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"md:prose-base prose-lg prose-zinc prose-invert text-opacity-90"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;The Banana Wrasse is known for its vibrant yellow color and elongated body, resembling a banana.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This active and playful fish thrives in tropical reef environments, often seen darting among corals and
              rocks.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

          &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
            &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-semibold px-6 bg-zinc-950/50 backdrop-blur-md py-3 rounded-xl text-white/90 border-2 border-white/10 mt-6 glow:ring-1 glow:border-glow glow:ring-glow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Learn
            more&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"fish-1.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Banana Wrasse"&lt;/span&gt;
          &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-sm rounded-full object-cover w-40 h-40 border-4 border-white/5 md:order-2 order-1 glow glow:ring-1 glow:border-glow/[.5] glow:ring-glow shadow-inner"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"glow-overlay"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"--glow-color: #7c3aed"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Fish 2 --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative glow-capture"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-zinc-900/50 border-4 border-white/5 rounded-2xl p-10 shadow-lg shadow-black/80 flex md:flex-row flex-col flex-wrap md:items-start items-center md:justify-between justify-center gap-6 backdrop-blur-md glow glow:ring-1 glow:border-glow glow:ring-glow glow:bg-glow/[.15]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 md:order-1 order-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"opacity-50"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Tropical fish&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl tracking-tighter mb-3 glow:text-glow/[.15]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Anthia&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"md:prose-base prose-lg prose-zinc prose-invert text-opacity-90"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Anthias fish are renowned for their dazzling, multi-hued scales, ranging from pinks and purples to
              oranges
              and yellows.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
              They form dynamic, shimmering schools in coral reefs, adding a burst of color and life to the underwater
              world.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
            &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-semibold px-6 bg-zinc-950/50 backdrop-blur-md py-3 rounded-xl text-white/90 border-2 border-white/10 mt-6 glow:ring-1 glow:border-glow glow:ring-glow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Learn
            more&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"fish-2.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Banana Wrasse"&lt;/span&gt;
          &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-sm rounded-full object-cover w-40 h-40 border-4 border-white/5 md:order-2 order-1 glow glow:ring-1 glow:border-glow/[.5] glow:ring-glow shadow-inner"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"glow-overlay"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"--glow-color: #f43f5e"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Fish 3 --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative glow-capture"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-zinc-900/50 border-4 border-white/5 rounded-2xl p-10 shadow-lg shadow-black/80 flex md:flex-row flex-col flex-wrap md:items-start items-center md:justify-between justify-center gap-6 backdrop-blur-md glow glow:ring-1 glow:border-glow glow:ring-glow glow:bg-glow/[.15]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 md:order-1 order-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"opacity-50"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Tropical fish&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl tracking-tighter mb-3 glow:text-glow/[.15]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Lipstick Tang&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"md:prose-base prose-lg prose-zinc prose-invert text-opacity-90"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;The Lipstick Tang, distinct for its striking blue body and vivid red-orange lips, resembles a vibrant
              splash
              of lipstick
              in the ocean. &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This eye-catching fish is a favorite among reef enthusiasts, gracefully navigating through coral
              gardens
              with elegance.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
            &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-semibold px-6 bg-zinc-950/50 backdrop-blur-md py-3 rounded-xl text-white/90 border-2 border-white/10 mt-6 glow:ring-1 glow:border-glow glow:ring-glow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Learn
            more&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"fish-3.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Banana Wrasse"&lt;/span&gt;
          &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-sm rounded-full object-cover w-40 h-40 border-4 border-white/5 md:order-2 order-1 glow glow:ring-1 glow:border-glow/[.5] glow:ring-glow shadow-inner"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"glow-overlay"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"--glow-color: #0ea5e9"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-sm text-white/60 text-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://webcrunch.com"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;webcrunch.com&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/main.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The end result
&lt;/h2&gt;

&lt;p&gt;The end result is super cool! I love this effect. We’re tricking ourselves visually with the by cloning the cards and using more modern CSS to make a neat glow effect.&lt;/p&gt;

&lt;p&gt;One limitation I discovered is the ability to do any major transition or transform effects. Because the element is cloned it looks way off if you attempt it.&lt;/p&gt;

&lt;p&gt;From here you can experiment with different glow colors, sizes, and transition effects to personalize the glow effect for your own needs.&lt;/p&gt;

&lt;p&gt;I hope you found this tutorial useful. If you enjoyed it be sure to share the link with your friends.&lt;/p&gt;

&lt;h3&gt;
  
  
  You might also enjoy these articles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/justalever/how-i-create-tailwind-css-plugins-from-scratch-3dfm-temp-slug-1481899"&gt;How I Create Tailwind CSS Plugins From Scratch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webcrunch.com/posts/code-a-mega-menu-with-tailwind-css"&gt;Code a mega menu with Tailwind CSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webcrunch.com/posts/re-create-airbnbs-home-page-with-tailwind-css"&gt;Re-create Airbnb's Home Page with Tailwind CSS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>tailwindcss</category>
      <category>css</category>
    </item>
    <item>
      <title>How to use Bun with Ruby on Rails</title>
      <dc:creator>Andy Leverenz</dc:creator>
      <pubDate>Sun, 07 Jan 2024 19:18:42 +0000</pubDate>
      <link>https://forem.com/justalever/how-to-use-bun-with-ruby-on-rails-39ll</link>
      <guid>https://forem.com/justalever/how-to-use-bun-with-ruby-on-rails-39ll</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/XzXKZduCvII"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In the Ruby on Rails world, we often leverage tools like &lt;a href="https://nodejs.org/en"&gt;node.js&lt;/a&gt; and &lt;a href="https://yarnpkg.com/"&gt;yarn&lt;/a&gt; as a build step for front-end tooling like Tailwind CSS, Stimulus.js, and many other front-end technologies.&lt;/p&gt;

&lt;p&gt;As &lt;a href="https://twitter.com/dhh/status/1740119521443340740"&gt;more hype&lt;/a&gt; grows about removing the “build” from the equation, I, for one, don’t necessarily see the additional dependencies and tooling as a bad thing. It's made so much of what I do way more efficient.&lt;/p&gt;

&lt;p&gt;A new approach called &lt;a href="https://bun.sh/"&gt;Bun&lt;/a&gt; has entered the scene and offers those same benefits and more. Most promising is the massive leap in build speed. I heard about this addition to Rails and have meant to dive in to try it out for some time. This blog post and video is an answer to that desire.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Bun?
&lt;/h2&gt;

&lt;p&gt;Bun is an all-in-one toolkit for JavaScript and TypeScript apps. It ships as a single executable called &lt;code&gt;bun​&lt;/code&gt;. You can compare it to npm and yarn.&lt;/p&gt;

&lt;p&gt;On the &lt;a href="https://bun.sh/"&gt;marketing site&lt;/a&gt; there's a section that reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Replace &lt;code&gt;yarn&lt;/code&gt; with &lt;code&gt;bun install&lt;/code&gt; to get 30x faster package installs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So you can see why I might be intrigued. To be 100% fair, I don’t leverage a lot of JavaScript. Ruby on Rails is my go-to for more features. In the event I need more interactivity, I reach out to Stimulus.js. That being said, if I stand to benefit from quicker installs of packages and build times, I will gravitate toward that route.&lt;/p&gt;

&lt;p&gt;Here are the design goals as quoted from Bun’s website&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Bun is designed from the ground up with today's JavaScript ecosystem in mind.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt;. Bun processes start &lt;a href="https://twitter.com/jarredsumner/status/1499225725492076544"&gt;4x faster than Node.js&lt;/a&gt; currently (try it yourself!)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript &amp;amp; JSX support&lt;/strong&gt;. You can directly execute &lt;code&gt;.jsx&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, and &lt;code&gt;.tsx&lt;/code&gt; files; Bun's transpiler converts these to vanilla JavaScript before execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ESM &amp;amp; CommonJS compatibility&lt;/strong&gt;. The world is moving towards ES modules (ESM), but millions of packages on npm still require CommonJS. Bun recommends ES modules, but supports CommonJS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web-standard APIs&lt;/strong&gt;. Bun implements standard Web APIs like &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;WebSocket&lt;/code&gt;, and &lt;code&gt;ReadableStream&lt;/code&gt;. Bun is powered by the JavaScriptCore engine, which is developed by Apple for Safari, so some APIs like &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Headers"&gt;&lt;code&gt;Headers&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/URL"&gt;&lt;code&gt;URL&lt;/code&gt;&lt;/a&gt; directly use &lt;a href="https://github.com/oven-sh/bun/blob/HEAD/src/bun.js/bindings/webcore/JSFetchHeaders.cpp"&gt;Safari's implementation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js compatibility&lt;/strong&gt;. In addition to supporting Node-style module resolution, Bun aims for full compatibility with built-in Node.js globals (&lt;code&gt;process&lt;/code&gt;, &lt;code&gt;Buffer&lt;/code&gt;) and modules (&lt;code&gt;path&lt;/code&gt;, &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;http&lt;/code&gt;, etc.) &lt;em&gt;This is an ongoing effort that is not complete.&lt;/em&gt; Refer to the &lt;a href="https://bun.sh/docs/runtime/nodejs-apis"&gt;compatibility page&lt;/a&gt; for the current status.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Bun is more than a runtime. The long-term goal is to be a cohesive, infrastructural toolkit for building apps with JavaScript/TypeScript, including a package manager, transpiler, bundler, script runner, test runner, and more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Bun with Rails
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://github.com/rails/jsbundling-rails/pull/167"&gt;a recent pull request to the jsbundling-rails gem&lt;/a&gt;, support for Bun was added to Rails as a JavaScript runtime.&lt;/p&gt;

&lt;p&gt;You can install it like other options, including &lt;code&gt;build,&lt;/code&gt; &lt;code&gt;rollup,&lt;/code&gt; and &lt;code&gt;webpack.&lt;/code&gt; I usually reach for &lt;code&gt;esbuild&lt;/code&gt; but wanted to try &lt;code&gt;bun&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a demo Rails app using bun
&lt;/h3&gt;

&lt;p&gt;Let’s create a simple app and give &lt;code&gt;bun&lt;/code&gt; a shot&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new bun_demo &lt;span class="nt"&gt;-c&lt;/span&gt; tailwind &lt;span class="nt"&gt;-j&lt;/span&gt; bun
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This installs a vanilla Rails application with Tailwind CSS and Bun for the front end.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about existing apps?
&lt;/h3&gt;

&lt;p&gt;Do you have an existing app and want to use Bun? Assuming you’ve installed the &lt;code&gt;jsbundling-rails&lt;/code&gt; gem, you can run the following task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./bin/rails javascript:install:bun
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Preview the &lt;code&gt;bun.config.js&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;When you run the new rails app command above in the latest version of Rails (7.1), a new &lt;code&gt;bun.config.js&lt;/code&gt; file is created in the project's root. Inside is the following code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;external&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;entrypoints&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;app/javascript/application.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;outdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/assets/builds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--watch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&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 failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logs&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 failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--watch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app/javascript&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;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`File changed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Rebuilding...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we have a general configuration for &lt;code&gt;bun&lt;/code&gt;, which declares the entry point of the app and the output points.&lt;/p&gt;

&lt;p&gt;The configuration also watches for changes within the &lt;code&gt;app/javascript&lt;/code&gt; directory, so you needn't refresh the page as you code. This was a massive milestone in front-end web development, and now we all seem to take it for granted :).&lt;/p&gt;

&lt;p&gt;Notice Stimulus.js is installed by default and preconfigured to “Just Work". Remember the motto “Convention Over Configuration”? that’s at play here, and I love this stuff!&lt;/p&gt;

&lt;p&gt;Much like &lt;code&gt;esbuild&lt;/code&gt;, a file builds to the &lt;code&gt;app/assets/builds&lt;/code&gt; directory in real-time. In this case, it would be called &lt;code&gt;application.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This configuration file gets processed with a script inside the &lt;code&gt;package.json&lt;/code&gt; file. There I have the following scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bun bun.config.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build:css"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because I installed Tailwind CSS and Bun, we see both scripts present.&lt;/p&gt;

&lt;p&gt;Finally, in the current version of Rails (7.1) there’s a &lt;code&gt;Procfile.dev&lt;/code&gt; file that gets sourced to a ruby gem called &lt;a href="https://github.com/ddollar/foreman"&gt;foreman&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That file bundles all the scripts we need to run into one set of processes giving us the ability to run one command to boot the rails app and build the assets.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Lovely, although a spiderweb of stuff, right? Luckily, it works out of the box, and you don’t have to pre-configure anything to install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not so fast!
&lt;/h2&gt;

&lt;p&gt;If you’re following along and you ran &lt;code&gt;bin/dev&lt;/code&gt; in the previous step, you might see an error like this in your logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unknown &lt;span class="nb"&gt;command&lt;/span&gt;: bun run build &lt;span class="nt"&gt;--watch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bun still needs to be installed on your system if you see this. There are several ways to go about installing it. I will reach for &lt;a href="https://brew.sh/"&gt;homebrew&lt;/a&gt;, given I’m on a Mac. Read &lt;a href="https://bun.sh/docs/installation"&gt;the docs&lt;/a&gt; for a different setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap oven-sh/bun 
brew &lt;span class="nb"&gt;install &lt;/span&gt;bun
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can check if it’s installed by running&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Bun is super similar to yarn in syntax and key commands. You can use it like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add &lt;span class="nt"&gt;-d&lt;/span&gt; tailwindcss
bun add @hotwired/turbo-rails @hotwired/stimulus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run &lt;code&gt;bin/dev&lt;/code&gt; we need to install dependencies using &lt;code&gt;bun&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;# bun install v1.0.20 (09d51486)&lt;/span&gt;
&lt;span class="c"&gt;# 12 packages installed [1.66s]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This happens quickly, but while Rails created the Stimulus and Hotwire files inside &lt;code&gt;app/javascript&lt;/code&gt;, the dependencies we need aren't installed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add @hotwired/stimulus @hotwired/turbo-rails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now running &lt;code&gt;bin/dev&lt;/code&gt;, we should be set up for success!&lt;/p&gt;

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

&lt;p&gt;Well, build cool stuff like normal! With bun under the hood for JavaScript, you get the added benefits of speed and compatibility without changing too much “muscle memory”.&lt;/p&gt;

&lt;p&gt;Personally, I have used yarn and &lt;a href="https://esbuild.github.io/"&gt;esbuild&lt;/a&gt; exclusively since&lt;a href="https://github.com/rails/jsbundling-rails"&gt;jsbundiling-rails&lt;/a&gt; and &lt;a href="https://github.com/rails/cssbundling-rails"&gt;cssbundling-rails&lt;/a&gt; entered the scene. I’m excited to give Bun a run for its money and will likely switch to it.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>bunjs</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Digging into Solid Queue and Rails</title>
      <dc:creator>Andy Leverenz</dc:creator>
      <pubDate>Sun, 07 Jan 2024 18:21:01 +0000</pubDate>
      <link>https://forem.com/justalever/digging-into-solid-queue-and-rails-164a</link>
      <guid>https://forem.com/justalever/digging-into-solid-queue-and-rails-164a</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/imU_9_C4UcQ"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In the realm of Ruby on Rails and background job processing, &lt;a href="https://github.com/basecamp/solid_queue"&gt;Solid Queue&lt;/a&gt; is the newest contender. Solid Queue is a robust alternative to well-established solutions like &lt;a href="https://github.com/bensheldon/good_job"&gt;good_job&lt;/a&gt; and &lt;a href="https://github.com/sidekiq/sidekiq"&gt;Sidekiq&lt;/a&gt;. This guide explores Solid Queue, delving into its distinctive features and comparing it with its counterparts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Solid Queue?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/basecamp/solid_queue"&gt;Solid Queue&lt;/a&gt; is an open-source background processing library tailored for Ruby on Rails applications. Its primary objective is to efficiently manage asynchronous tasks, ensuring your application's performance remains rock-solid even during peak traffic and demanding workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features of Solid Queue:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reliability:&lt;/strong&gt; Solid Queue places a premium on reliability and data integrity. It guarantees that jobs are processed precisely once, eliminating the risk of duplicates or data loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Support:&lt;/strong&gt; Unlike some competing solutions, Solid Queue seamlessly integrates with database transactions, ensuring consistent data when both your application and background jobs interact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency Control:&lt;/strong&gt; Solid Queue offers granular control over concurrency, enabling you to specify the number of workers for each job type and optimize resource utilization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active Job Compatibility:&lt;/strong&gt; If your Rails application relies on Active Job, you'll appreciate Solid Queue's effortless compatibility, simplifying migration and adoption.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How Solid Queue Sets Itself Apart
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Integrity:&lt;/strong&gt; Solid Queue is committed to data integrity, which is critical for applications handling sensitive information or intricate business logic. It guarantees that jobs are processed reliably, eliminating the possibility of duplicates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Transactions:&lt;/strong&gt; Integrating Solid Queue with database transactions is a standout feature. In contrast, GoodJob and Sidekiq may necessitate additional configuration through something like Redis and management to maintain transactional consistency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency Control:&lt;/strong&gt; While all three solutions offer concurrency control, Solid Queue's flexibility in specifying worker counts for different job types empowers you to fine-tune resource allocation for optimal performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Examples
&lt;/h3&gt;

&lt;p&gt;To show how to use SolidQueue in a Ruby on Rails application, you'll need to take the following steps on a new Rails application.&lt;/p&gt;

&lt;p&gt;Install the gem. Add the following to your &lt;code&gt;Gemfile&lt;/code&gt; and run &lt;code&gt;bundle install&lt;/code&gt; .&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="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"solid_queue"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the installer&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails generate solid_queue:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This adds a few migrations, replaces a few configurations in the &lt;code&gt;test,&lt;/code&gt; &lt;code&gt;development,&lt;/code&gt; and &lt;code&gt;production&lt;/code&gt; environment, and adds a &lt;code&gt;solid_queue.yml&lt;/code&gt; config file to the app. All the values are commented out by default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gsub config/environments/development.rb
        gsub config/environments/test.rb
        gsub config/environments/production.rb
      create config/solid_queue.yml
       rails railties:install:migrations &lt;span class="nv"&gt;FROM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;solid_queue
Copied migration 20240102191754_create_solid_queue_tables.solid_queue.rb from solid_queue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configuration wasn’t added to my &lt;code&gt;development.rb&lt;/code&gt; environment file but made it to the &lt;code&gt;production.rb&lt;/code&gt; file. You may need to append the following to your preferred environment configurations if you aren't adding it to &lt;code&gt;config/application.rb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;config.active_job.queue_adapter &lt;span class="o"&gt;=&lt;/span&gt; :solid_queue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the &lt;code&gt;config/solid_queue.yml&lt;/code&gt; configuration file, be sure to uncomment the defaults.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;default&lt;/span&gt;
  &lt;span class="na"&gt;dispatchers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;polling_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;batch_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;
  &lt;span class="na"&gt;workers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;queues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
      &lt;span class="na"&gt;threads&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;processes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;polling_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;

&lt;span class="na"&gt;development&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default&lt;/span&gt;

&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default&lt;/span&gt;

&lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be sure to configure Solid Queue as your Active Job queue adapter&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="c1"&gt;# config/application.rb&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:solid_queue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the migrations&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates several tables related to different states of a given job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;==&lt;/span&gt; 20240102191754 CreateSolidQueueTables: migrating &lt;span class="o"&gt;===========================&lt;/span&gt;
&lt;span class="nt"&gt;--&lt;/span&gt; create_table&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_jobs&lt;span class="o"&gt;)&lt;/span&gt;
   -&amp;gt; 0.0024s
&lt;span class="nt"&gt;--&lt;/span&gt; create_table&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_scheduled_executions&lt;span class="o"&gt;)&lt;/span&gt;
   -&amp;gt; 0.0009s
&lt;span class="nt"&gt;--&lt;/span&gt; create_table&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_ready_executions&lt;span class="o"&gt;)&lt;/span&gt;
   -&amp;gt; 0.0012s
&lt;span class="nt"&gt;--&lt;/span&gt; create_table&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_claimed_executions&lt;span class="o"&gt;)&lt;/span&gt;
   -&amp;gt; 0.0009s
&lt;span class="nt"&gt;--&lt;/span&gt; create_table&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_blocked_executions&lt;span class="o"&gt;)&lt;/span&gt;
   -&amp;gt; 0.0009s
&lt;span class="nt"&gt;--&lt;/span&gt; create_table&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_failed_executions&lt;span class="o"&gt;)&lt;/span&gt;
   -&amp;gt; 0.0007s
&lt;span class="nt"&gt;--&lt;/span&gt; create_table&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_pauses&lt;span class="o"&gt;)&lt;/span&gt;
   -&amp;gt; 0.0007s
&lt;span class="nt"&gt;--&lt;/span&gt; create_table&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_processes&lt;span class="o"&gt;)&lt;/span&gt;
   -&amp;gt; 0.0009s
&lt;span class="nt"&gt;--&lt;/span&gt; create_table&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_semaphores&lt;span class="o"&gt;)&lt;/span&gt;
   -&amp;gt; 0.0012s
&lt;span class="nt"&gt;--&lt;/span&gt; add_foreign_key&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_blocked_executions, :solid_queue_jobs, &lt;span class="o"&gt;{&lt;/span&gt;:column&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;:job_id, :on_delete&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;:cascade&lt;span class="o"&gt;})&lt;/span&gt;
   -&amp;gt; 0.0062s
&lt;span class="nt"&gt;--&lt;/span&gt; add_foreign_key&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_claimed_executions, :solid_queue_jobs, &lt;span class="o"&gt;{&lt;/span&gt;:column&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;:job_id, :on_delete&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;:cascade&lt;span class="o"&gt;})&lt;/span&gt;
   -&amp;gt; 0.0054s
&lt;span class="nt"&gt;--&lt;/span&gt; add_foreign_key&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_failed_executions, :solid_queue_jobs, &lt;span class="o"&gt;{&lt;/span&gt;:column&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;:job_id, :on_delete&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;:cascade&lt;span class="o"&gt;})&lt;/span&gt;
   -&amp;gt; 0.0050s
&lt;span class="nt"&gt;--&lt;/span&gt; add_foreign_key&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_ready_executions, :solid_queue_jobs, &lt;span class="o"&gt;{&lt;/span&gt;:column&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;:job_id, :on_delete&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;:cascade&lt;span class="o"&gt;})&lt;/span&gt;
   -&amp;gt; 0.0061s
&lt;span class="nt"&gt;--&lt;/span&gt; add_foreign_key&lt;span class="o"&gt;(&lt;/span&gt;:solid_queue_scheduled_executions, :solid_queue_jobs, &lt;span class="o"&gt;{&lt;/span&gt;:column&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;:job_id, :on_delete&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;:cascade&lt;span class="o"&gt;})&lt;/span&gt;
   -&amp;gt; 0.0053s
&lt;span class="o"&gt;==&lt;/span&gt; 20240102191754 CreateSolidQueueTables: migrated &lt;span class="o"&gt;(&lt;/span&gt;0.0381s&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==================&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start Solid Queue’s supervisor to be able to run jobs. This should be a new terminal instance.&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;rake solid_queue:start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define and enqueue jobs as you would with Active Job:&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="c1"&gt;# app/jobs/sample_job.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SampleJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;
    &lt;span class="c1"&gt;# Your job logic here&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Enqueue the job&lt;/span&gt;
&lt;span class="no"&gt;SampleJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;


&lt;span class="n"&gt;rails&lt;/span&gt; &lt;span class="n"&gt;console&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;SampleJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty simple, right? I love that it lives within the framework and doesn’t make me learn a complicated new standard. I come from Sidekiq, and the workflow is highly similar, minus a few bits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Failed jobs and retries
&lt;/h2&gt;

&lt;p&gt;Solid Queue does not have an automatic retry feature; instead, it depends on &lt;a href="https://edgeguides.rubyonrails.org/active_job_basics.html#retrying-or-discarding-failed-jobs"&gt;Active Job for this functionality&lt;/a&gt;. Any failed jobs will be stored in the system, and a record of the failed execution will be created in the &lt;code&gt;solid_queue_failed_executions&lt;/code&gt; table. These failed jobs will remain in the system until manually discarded or re-enqueued.&lt;/p&gt;

&lt;p&gt;The team plans to introduce a UI component called &lt;em&gt;Mission Control&lt;/em&gt; that allows you to examine and diagnose any specific job or group of jobs efficiently.&lt;/p&gt;

&lt;p&gt;For now, you can manually intervene with these methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;failed_execution &lt;span class="o"&gt;=&lt;/span&gt; SolidQueue::FailedExecution.find&lt;span class="o"&gt;(&lt;/span&gt;...&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="c"&gt;# Find the failed execution related to your job&lt;/span&gt;
failed_execution.error &lt;span class="c"&gt;# inspect the error&lt;/span&gt;

failed_execution.retry &lt;span class="c"&gt;# This will re-enqueue the job as if it was enqueued for the first time&lt;/span&gt;
failed_execution.discard &lt;span class="c"&gt;# This will delete the job from the system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This seems nice for the ability to manually retry if necessary in your app’s logic rather than having it be arbitrary. Some might argue the latter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding some UI in the meantime
&lt;/h2&gt;

&lt;p&gt;I noticed on Twitter a library called &lt;a href="https://github.com/virolea/panoptic"&gt;Panoptic&lt;/a&gt;. The simple UI was built with Bootstrap CSS to provide a GUI for Solid Queue.&lt;/p&gt;

&lt;p&gt;You can add it much like Sidekiq if you’re familiar with that configuration style. It’s an engine that bolts on the app.&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="c1"&gt;# add to your Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"panoptic"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your &lt;code&gt;config/routes.rb&lt;/code&gt; file, add the following one-liner&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="c1"&gt;# config/routes.rb&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;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;mount&lt;/span&gt; &lt;span class="no"&gt;Panoptic&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Engine&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"/panoptic"&lt;/span&gt;
  &lt;span class="c1"&gt;# more code&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can visit &lt;code&gt;localhost:3000:/panoptic&lt;/code&gt; to see it in action.&lt;/p&gt;

&lt;p&gt;Again, the team has stated that something called &lt;em&gt;Mission Control&lt;/em&gt; is on the near horizon, so I wouldn't depend heavily on this library. It is something, though!&lt;/p&gt;

&lt;h3&gt;
  
  
  In pursuit of increased developer happiness with Puma
&lt;/h3&gt;

&lt;p&gt;While running &lt;code&gt;bundle exec rake solid_queue:start&lt;/code&gt; each time you need background job processing, it works fine; a handy Puma plugin integration in Solid Queue allows for some nice automations. When Puma is running (rails server), the processing of your job will kick in alongside it.&lt;/p&gt;

&lt;p&gt;I need to add a one-liner to my &lt;code&gt;puma.rb&lt;/code&gt; file.&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="c1"&gt;# config/puma.rb&lt;/span&gt;
&lt;span class="n"&gt;plugin&lt;/span&gt; &lt;span class="ss"&gt;:solid_queue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Solid Queue is a formidable choice for background job processing in Ruby on Rails applications. It prioritizes reliability, data integrity, and seamless database transaction integration.&lt;/p&gt;

&lt;p&gt;While GoodJob and Sidekiq remain excellent alternatives, Solid Queue's unique attributes make it a compelling candidate for your next project without additional dependencies or resources. I think you will need to consider the extra database space it might reserve, but hopefully, it shouldn't be enough to cause too much concern.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>tutorial</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Digging into Turbo 8's Morphing Feature in Ruby on Rails</title>
      <dc:creator>Andy Leverenz</dc:creator>
      <pubDate>Fri, 22 Dec 2023 20:18:06 +0000</pubDate>
      <link>https://forem.com/justalever/digging-into-turbo-8s-morphing-feature-in-ruby-on-rails-4ij9</link>
      <guid>https://forem.com/justalever/digging-into-turbo-8s-morphing-feature-in-ruby-on-rails-4ij9</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/wURcezOECD0"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Inspired by Jorge Manrubia’s &lt;a href="https://www.youtube.com/watch?v=m97UsXa6HFg&amp;amp;t=4s"&gt;talk from Railsworld&lt;/a&gt; I wanted to try out morphing which is coming to &lt;a href="https://github.com/hotwired/turbo"&gt;Turbo 8&lt;/a&gt; very soon.&lt;/p&gt;

&lt;p&gt;On October 9th, 2023 Jorge &lt;a href="https://dev.37signals.com/a-happier-happy-path-in-turbo-with-morphing/"&gt;published a blog post&lt;/a&gt; that was a precursor to the talk. I recommend giving it a read.&lt;/p&gt;

&lt;p&gt;The gist of the blog post and talk is that Turbo frames and streams are useful but often cumbersome to integrate since they are highly focused containers of logic.&lt;/p&gt;

&lt;p&gt;They won’t be going away but might be more of a &lt;em&gt;special-use&lt;/em&gt; tool coming up with the introduction of morphing which could be a more convenient and useful “default” much like the standard full-page reloads of historical Rails apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Discovering the problem
&lt;/h3&gt;

&lt;p&gt;The Basecamp team has been working on integrating a calendar into their HEY product. In building the new feature, they quickly spotted the constraints of turbo frames and streams. Having to broadcast and update many items on a given page is problematic and overly complex, so they looked for a better approach, one much closer to the default Rails full-page reload conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is morphing?
&lt;/h2&gt;

&lt;p&gt;No, this doesn’t relate to Power Rangers, though one could wish!&lt;/p&gt;

&lt;p&gt;Morphing is the process of merging one DOM into another without too many side effects. It’s not necessarily natural but the perception our eyes see makes it feel as such.&lt;/p&gt;

&lt;p&gt;Morphing isn’t new, but it is to Turbo 8. The Basecamp team chose &lt;a href="https://github.com/bigskysoftware/idiomorph"&gt;idiomorph&lt;/a&gt; as a library to help with the new features. It's a JavaScript library for morphing from one DOM tree to another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The TL;DR;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Morphing provides smoother updates everywhere rather than selective updates like turbo streams and turbo frames.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it to practice
&lt;/h2&gt;

&lt;p&gt;Let’s make a quick blog to put these new features to work. I prefer &lt;code&gt;esbuild&lt;/code&gt; to &lt;code&gt;importmaps&lt;/code&gt; so I passed a flag to declare that. Feel free to skip that part.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails new morphblog -j esbuild

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add Tailwind CSS
&lt;/h3&gt;

&lt;p&gt;I’m going to scaffold our models coming up so to make the views pre-styled and save myself some time we’ll leverage the dedicated &lt;a href="https://github.com/rails/tailwindcss-rails"&gt;tailwindcss-rails&lt;/a&gt;gem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add tailwindcss-rails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run the installer&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails tailwindcss:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app is now accessible at &lt;a href="https://dev.tolocalhost:5000"&gt;localhost:5000&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Turbo
&lt;/h2&gt;

&lt;p&gt;Add the &lt;a href="https://github.com/hotwired/turbo-rails/releases/tag/v2.0.0-beta.2"&gt;latest beta of Turbo&lt;/a&gt;(turbo-rails gem) (this will change of course once the official version is released)&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="c1"&gt;# Gemfile&lt;/span&gt;

&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"turbo-rails"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 2.0.0-beta.2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run&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;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don’t forget to update the &lt;a href="https://github.com/hotwired/turbo/releases"&gt;node version of Turbo&lt;/a&gt; as well&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add @hotwired/turbo@8.0.0-beta.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create the blog
&lt;/h2&gt;

&lt;p&gt;Scaffold a &lt;code&gt;Post&lt;/code&gt; resource. We’ll make it reference a &lt;code&gt;User&lt;/code&gt; model that I'll create next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g scaffold Post title content:text user:references
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scaffold a &lt;code&gt;User&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g scaffold User name email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For the guide I’ll forgo worrying about authentication so we’re keeping this stupid simple for now.&lt;/p&gt;

&lt;p&gt;Create a User on the rails command line &lt;code&gt;rails console&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;User.create&lt;span class="o"&gt;(&lt;/span&gt;name: &lt;span class="s2"&gt;"Andy"&lt;/span&gt;, email: &lt;span class="s2"&gt;"andy@webcrunch.com"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

  TRANSACTION &lt;span class="o"&gt;(&lt;/span&gt;0.0ms&lt;span class="o"&gt;)&lt;/span&gt; begin transaction
  User Create &lt;span class="o"&gt;(&lt;/span&gt;0.5ms&lt;span class="o"&gt;)&lt;/span&gt; INSERT INTO &lt;span class="s2"&gt;"users"&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;, &lt;span class="s2"&gt;"email"&lt;/span&gt;, &lt;span class="s2"&gt;"created_at"&lt;/span&gt;, &lt;span class="s2"&gt;"updated_at"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; VALUES &lt;span class="o"&gt;(&lt;/span&gt;?, ?, ?, ?&lt;span class="o"&gt;)&lt;/span&gt; RETURNING &lt;span class="s2"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;, &lt;span class="s2"&gt;"Andy"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;, &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;, &lt;span class="s2"&gt;"andy@webcrunch.com"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;, &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"created_at"&lt;/span&gt;, &lt;span class="s2"&gt;"2023-12-21 20:31:06.848693"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;, &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"updated_at"&lt;/span&gt;, &lt;span class="s2"&gt;"2023-12-21 20:31:06.848693"&lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;
  TRANSACTION &lt;span class="o"&gt;(&lt;/span&gt;0.4ms&lt;span class="o"&gt;)&lt;/span&gt; commit transaction
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;#&amp;lt;User:0x00000001090775b0&lt;/span&gt;
 &lt;span class="nb"&gt;id&lt;/span&gt;: 1,
 name: &lt;span class="s2"&gt;"Andy"&lt;/span&gt;,
 email: &lt;span class="s2"&gt;"andy@webcrunch.com"&lt;/span&gt;,
 created_at: Thu, 21 Dec 2023 20:31:06.848693000 UTC +00:00,
 updated_at: Thu, 21 Dec 2023 20:31:06.848693000 UTC +00:00&amp;gt;
irb&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&lt;/span&gt;:002&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Update the associations
&lt;/h3&gt;

&lt;p&gt;After generating the resources we can define the associations in the models.&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="c1"&gt;# app/models/user.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:posts&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then in the &lt;code&gt;Post&lt;/code&gt; model:&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="c1"&gt;# app/models/post.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard Rails stuff!&lt;/p&gt;

&lt;h3&gt;
  
  
  Update routing
&lt;/h3&gt;

&lt;p&gt;Inside &lt;code&gt;config/routes.rb&lt;/code&gt; we can uncomment the line beginning with &lt;code&gt;root&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:posts&lt;/span&gt;
  &lt;span class="c1"&gt;# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html&lt;/span&gt;

  &lt;span class="c1"&gt;# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.&lt;/span&gt;
  &lt;span class="c1"&gt;# Can be used by load balancers and uptime monitors to verify that the app is live.&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"up"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"rails/health#show"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :rails_health_check&lt;/span&gt;

  &lt;span class="c1"&gt;# Defines the root path route ("/")&lt;/span&gt;
  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="s2"&gt;"posts#index"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stub out the controller
&lt;/h3&gt;

&lt;p&gt;Normally in the controller, you might define a current user based on some authentication strategy. To make this demo “just work” I’ll stub it out with some crappy code assigning the first &lt;code&gt;User&lt;/code&gt; directly :)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="c1"&gt;# assign the User record we just made for demo purposes&lt;/span&gt;
    &lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&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="n"&gt;post_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="no"&gt;User&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="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;
        &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;post_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Post was successfully created."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:show&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;location: &lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;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;
  
  
  Remove user references from the Post form
&lt;/h3&gt;

&lt;p&gt;Because we are doing this in our controller, we don’t need the &lt;code&gt;user_id&lt;/code&gt; field. Let's remove it so we are left with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"contents"&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;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"error_explanation"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;pluralize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; prohibited this post from being saved:&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;error&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_message&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_area&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;rows: &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Move the Post form to the index
&lt;/h3&gt;

&lt;p&gt;Let’s make our form super simple and display it above all the posts on the index. Maybe think of it like a makeshift “X” (formerly Twitter) app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&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;"w-full max-w-3xl mx-auto"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Posts&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"posts/form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"posts"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full mt-10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@posts&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here go ahead and create a post or two. You can use the console or go through the UI but we just need a post or two as dummy data to get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add new morph meta tag
&lt;/h2&gt;

&lt;p&gt;In your &lt;code&gt;application.html.erb&lt;/code&gt; layout file add the following within the &lt;code&gt;head&lt;/code&gt; tags.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_refreshes_with&lt;/span&gt; &lt;span class="ss"&gt;method: :morph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scroll: :preserve&lt;/span&gt; &lt;span class="cp"&gt;%&amp;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 erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Morphblog&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width,initial-scale=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csrf_meta_tags&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csp_meta_tag&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_refreshes_with&lt;/span&gt; &lt;span class="ss"&gt;method: :morph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scroll: :preserve&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class="s2"&gt;"tailwind"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"inter-font"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"data-turbo-track"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"reload"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"data-turbo-track"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"reload"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;javascript_include_tag&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"data-turbo-track"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"reload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;defer: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This instructs the app to "morph" the page while preserving the scroll position. Previously, Turbo would "replace" the entire page and "reset" the scroll position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update Post model
&lt;/h2&gt;

&lt;p&gt;Next, we need to broadcast the updates. With the new beta version of Turbo, we can add the following to our Posts model.&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="c1"&gt;# app/models/post.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
  &lt;span class="n"&gt;broadcasts_refreshes&lt;/span&gt; &lt;span class="c1"&gt;# add this line&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the posts#show view, we can add the &lt;code&gt;turbo_stream_from&lt;/code&gt; tag you might already be familiar with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&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-auto md:w-2/3 w-full flex"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Edit this post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edit_post_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline-block ml-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="s2"&gt;"Destroy this post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;method: :delete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Back to posts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;posts_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch &lt;a href="https://share.cleanshot.com/dR4jzvhg"&gt;a quick example&lt;/a&gt; of the “streamed” updates in action. I opened two windows, one being incognito. It kinda “just works” which is way cooler than tons of turbo frame tags and whatnot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating collections
&lt;/h2&gt;

&lt;p&gt;This is one area I’m not 100% sold on. It still works but there’s a big assumption with a collection you’ll have an association tied to it. That’s probably true most of the time but it feels a bit rigid. I might be overthinking it of course.&lt;/p&gt;

&lt;p&gt;Using the new morph features to make something like the posts index stream updates we need an association tied to it. So instead of just rendering all the posts, we’ll render the user’s posts and update our models.&lt;/p&gt;

&lt;p&gt;Here’s the &lt;code&gt;Post&lt;/code&gt; model&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="c1"&gt;# app/models/post.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="c1"&gt;# updates the timestamp when a post-commit (create, update, destroy) occurs&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;touch: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;broadcasts_refreshes&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;touch: true&lt;/code&gt; addition instructs the timestamp of the associated user when it's manipulated in some way.&lt;/p&gt;

&lt;p&gt;On the &lt;code&gt;User&lt;/code&gt; model we need to add the new method&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="c1"&gt;# app/models/user.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:posts&lt;/span&gt;
  &lt;span class="n"&gt;broadcasts_refreshes&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the index view, we’ll stub out what might normally be a &lt;code&gt;current_user&lt;/code&gt; with the associated user we defined for demonstration purposes.&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="c1"&gt;# app/controllers/posts_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:set_post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="sx"&gt;%i[show edit update destroy]&lt;/span&gt;

  &lt;span class="c1"&gt;# GET /posts or /posts.json&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
    &lt;span class="vi"&gt;@posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;posts&lt;/span&gt; &lt;span class="c1"&gt;# update to user posts&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# more stuff below&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we need to stream updates from that User specifically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&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;"w-full max-w-3xl mx-auto"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-4xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Posts&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"posts/form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"posts"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-w-full mt-10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@posts&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://share.cleanshot.com/HDSgNCsQ"&gt;Here it is in action&lt;/a&gt;. Pretty darn slick!&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Having dabbled with the beta I’m super excited about this update!&lt;/p&gt;

&lt;p&gt;It makes reaching for turbo frame tags more intentional versus necessary. There will no doubt be times when you need more sophistication but I think this is a huge improvement and one I’m excited to transition into existing apps leveraging a ton of turbo frames. The fact that I need to add about 3 lines of code is probably my favorite part. Cheers to the future of Rails!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Best Practices for Naming Models in Ruby on Rails</title>
      <dc:creator>Andy Leverenz</dc:creator>
      <pubDate>Sun, 17 Dec 2023 13:33:10 +0000</pubDate>
      <link>https://forem.com/justalever/best-practices-for-naming-models-in-ruby-on-rails-2pkp</link>
      <guid>https://forem.com/justalever/best-practices-for-naming-models-in-ruby-on-rails-2pkp</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/I3qyHJUNLn8"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In Ruby on Rails, naming models is more than just a convention. It's about clarity, efficiency, and ensuring your app is easy to manage. Here's a guide to help you name your models like the pros.&lt;/p&gt;

&lt;p&gt;P.S. I created a handy guide at &lt;a href="https://web-crunch.com/naming-models"&gt;https://web-crunch.com/naming-models&lt;/a&gt;. Bookmark it if you need a refresher every once in a while!&lt;/p&gt;

&lt;h3&gt;
  
  
  Singular and Capitalized
&lt;/h3&gt;

&lt;p&gt;Model names should be singular and capitalized. Rails uses this convention to look for the corresponding pluralized table name in the database.&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="c1"&gt;# Good&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Avoid&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Products&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keep it descriptive and clear
&lt;/h3&gt;

&lt;p&gt;Names should be self-explanatory. Don't forget to avoid abbreviations unless they are well-known.&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="c1"&gt;# Good&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShoppingCart&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Avoid&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SCart&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Organize with namespaces
&lt;/h3&gt;

&lt;p&gt;For complex apps, namespaces help in organizing models. Use modules to group related models.&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="c1"&gt;# Good&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Inventory&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Item&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Usage&lt;/span&gt;
&lt;span class="vi"&gt;@item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Intuitive Associations
&lt;/h3&gt;

&lt;p&gt;Keep it intuitive. If a &lt;code&gt;User&lt;/code&gt; has many &lt;code&gt;articles&lt;/code&gt;, the association should reflect that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:articles&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Acronyms and Initialisms
&lt;/h3&gt;

&lt;p&gt;If you're using acronyms, keep them uppercase.&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="c1"&gt;# Good&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HTTPRequest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Avoid&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Avoid Reserved Words
&lt;/h3&gt;

&lt;p&gt;Some words are reserved in Rails and should be avoided as model names (e.g., &lt;code&gt;Attribute&lt;/code&gt;, &lt;code&gt;Error&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Context-Specific Naming
&lt;/h3&gt;

&lt;p&gt;If your app has models that only make sense within a specific context, name them accordingly.&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="c1"&gt;# Good for a school management app&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GradeReport&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Good for an e-commerce app&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Avoid Ambiguity
&lt;/h3&gt;

&lt;p&gt;Names should be unambiguous, even if they end up being longer.&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="c1"&gt;# Good&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SubscriptionPayment&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Avoid&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Payment&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="c1"&gt;# This could be ambiguous in an app dealing with multiple payment types&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Composite Names
&lt;/h3&gt;

&lt;p&gt;For models representing a combination of entities, use clear composite names.&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="c1"&gt;# Good&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserSubscriptionHistory&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Avoid&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserHistory&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="c1"&gt;# This is vague about what history it refers to&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Polymorphic Associations
&lt;/h3&gt;

&lt;p&gt;In polymorphic associations, choose names that clearly indicate their versatile nature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Picture&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:imageable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;polymorphic: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Employee&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:pictures&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :imageable&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:pictures&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :imageable&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  STI (Single Table Inheritance)
&lt;/h3&gt;

&lt;p&gt;The model name should indicate its role if you use Single Table Inheritance.&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="c1"&gt;# Good&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Vehicle&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Car&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Vehicle&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Motorcycle&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Vehicle&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Remember, well-named models make your code cleaner and more accessible for others (or future you) to understand and maintain. I hope these examples add more depth to your understanding! Look for more articles like this to come. In the meantime, check out some related content:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/justalever/a-beginners-guide-to-ruby-on-rails-callbacks-4noh"&gt;A Beginner's Guide to Ruby on Rails Callbacks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/justalever/understanding-active-record-callbacks-2l2g"&gt;Understanding Active Record Callbacks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web-crunch.com/posts/understanding-active-record-associations"&gt;Understanding Active Record Associations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web-crunch.com/posts/understanding-active-record-migrations"&gt;Understanding Active Record Migrations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web-crunch.com/posts/understanding-ruby-on-rails-activerecord-validations"&gt;Understanding Ruby on Rails ActiveRecord Validations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;P.P.S. &lt;strong&gt;Reminder!&lt;/strong&gt; I created a handy guide at &lt;a href="https://web-crunch.com/naming-models"&gt;https://web-crunch.com/naming-models&lt;/a&gt;. Bookmark it if you need a refresher every once in a while!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Create a Tailwind CSS Plugins from Scratch</title>
      <dc:creator>Andy Leverenz</dc:creator>
      <pubDate>Wed, 06 Dec 2023 19:55:06 +0000</pubDate>
      <link>https://forem.com/justalever/how-i-create-a-tailwind-css-plugins-from-scratch-27l9</link>
      <guid>https://forem.com/justalever/how-i-create-a-tailwind-css-plugins-from-scratch-27l9</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/-maP6EyN-hg"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This tutorial guides you through creating a custom Tailwind CSS plugin to add a variety of button styles. I find it an excellent way to rinse and reuse button components in the multiple prototype-like apps I build yearly. We'll cover designs like solid, outline, and ghost buttons, each with customizable colors and hover states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Tailwind CSS plugins?
&lt;/h2&gt;

&lt;p&gt;From the Tailwind CSS docs,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Plugins let you register new styles for Tailwind to inject into the user’s stylesheet using JavaScript instead of CSS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is handy when leveraging reusable tailwind components or styles across different apps, websites, or environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Familiarity with Tailwind CSS is super useful to have!&lt;/li&gt;
&lt;li&gt;Node.js and yarn or npm installed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Install Vite.js
&lt;/h3&gt;

&lt;p&gt;I’ll be using &lt;a href="https://vitejs.dev/"&gt;vite.js&lt;/a&gt; to make use of Tailwind CSS. I reach for it when I need something more static in production. Folks who have followed me might know I usually use Ruby on Rails for my full-stack framework of choice. This guide applies to everything in that environment, too, but this guide is less about Rails.&lt;/p&gt;

&lt;p&gt;Alright, let’s install Vite. You’ll want to create a new app using the following command. Follow the steps and choose vanilla JavaScript to keep things simple.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;When naming the project, I chose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailwindcss-button-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choose vanilla JavaScript for any option that prompts you and run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;tailwindcss-button-plugin
yarn
yarn dev

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

&lt;/div&gt;



&lt;p&gt;This should boot a development server and give you a localhost link to visit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Tailwind CSS
&lt;/h3&gt;

&lt;p&gt;Next, we need Tailwind CSS installed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add tailwindcss postcss autoprefixer &lt;span class="nt"&gt;-D&lt;/span&gt;
npx tailwindcss init &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two commands install Tailwind CSS dependencies in the project and create a &lt;code&gt;tailwind.config.js&lt;/code&gt; file and a &lt;code&gt;postcss.config.js&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Created Tailwind CSS config file: tailwind.config.js
Created PostCSS config file: postcss.config.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the &lt;code&gt;tailwind.config.js&lt;/code&gt; file, we can add the following.&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="cm"&gt;/** @type {import('tailwindcss').Config} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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="s2"&gt;./index.html&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;./**/*.{js,ts,jsx,tsx}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&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;Inside the &lt;code&gt;style.css&lt;/code&gt; file, I'll remove the current contents and add the following Tailwind directives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* style.css */&lt;/span&gt;

&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigating back to the localhost path Vite supplied us, we should see the browser reset. Tailwind CSS is working, but it’s not apparent until we code.&lt;/p&gt;

&lt;p&gt;I’ll remove all default content from the app so we have a clean slate.&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="c"&gt;&amp;lt;!-- index.html --&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"icon"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/svg+xml"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/vite.svg"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Tailwind CSS Button Plugin Tutorial&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/main.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/html&amp;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;// main.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./style.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;You can just delete all the SVG assets, too. With this configuration complete, we are ready to write the plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Setting the Plugin File
&lt;/h2&gt;

&lt;p&gt;In the app's root directory, create a new file for the plugin named &lt;code&gt;tailwind-custom-buttons.js&lt;/code&gt;. You can call this whatever you like.&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;// tailwind-custom-buttons.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="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;addComponents&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Our button styles will be added here&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll need to export a function with built-in helper functions that give us access to various parts of the tailwind library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Writing the Button Styles
&lt;/h2&gt;

&lt;p&gt;We'll create several button styles: solid, outline, and ghost. Here's how you can define them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;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="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;addComponents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buttons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn&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;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.5rem 1rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;borderRadius.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;600&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inline-block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;textAlign&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&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="s1"&gt;.btn-solid&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;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.blue.500&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;:hover&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;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.blue.600&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn-outline&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;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transparent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.blue.500&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2px solid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;borderColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.blue.500&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;:hover&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;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.blue.500&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn-ghost&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;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transparent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.blue.500&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;:hover&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;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.blue.100&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="c1"&gt;// Additional styles can be added here&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;addComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Testing the Plugin
&lt;/h2&gt;

&lt;p&gt;Include the plugin in your &lt;code&gt;tailwind.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tailwind.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="c1"&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;./tailwind-custom-buttons.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;// Other plugins...&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;You can just run your build process to see the new button styles.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;index.html&lt;/code&gt; file I added the following&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- index.html --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"icon"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/svg+xml"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/vite.svg"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Tailwind CSS Button Plugin Tutorial&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;body&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-stone-50 text-stone-800"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl mx-auto my-24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-3xl pb-6 text-stone-800 text-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Tailwind CSS Button Plugin Tutorial&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center justify-center gap-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-solid"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Solid&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-outline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Outline&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-ghost"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Ghost&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-primary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Primary&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-danger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Danger&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-custom"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Custom&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/main.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Advanced Customization
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Responsive and State Variants
&lt;/h3&gt;

&lt;p&gt;You can easily add responsive and state variants to your buttons:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;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="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;addComponents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;variants&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buttons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ... your button styles&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;addComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;variants&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;responsive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focus&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;This unlocks the ability to change button styles that respond to interactions and screen viewport widths.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Theming
&lt;/h3&gt;

&lt;p&gt;Allow customization of button colors by using the theme's color configuration:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn-custom&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="nl"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.custom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.teal.500&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;:hover&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="nl"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.customHover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colors.teal.600&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="c1"&gt;// Similarly, add .btn-secondary, .btn-danger, etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;tailwind.config.js&lt;/code&gt;, add custom colors if needed:&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="cm"&gt;/** @type {import('tailwindcss').Config} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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="s2"&gt;./index.html&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;./**/*.{js,ts,jsx,tsx}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#f79b4b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;customHover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#e38a3c&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;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="s2"&gt;./tailwind-custom-buttons&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why not add these using CSS?
&lt;/h2&gt;

&lt;p&gt;Well, you totally can do that. In fact, it’s pretty simple to do, but I think it boils down to reusability. If you’re on a team and build out a series of custom components like this one, you could effectively publish it as a separate repo somewhere and include it on new projects as we advance. With CSS, that’s a little harder to manage but not impossible.&lt;/p&gt;

&lt;p&gt;I’ve primarily gone the CSS route since most of my custom CSS is relative to the application or website I’m creating and it didn’t make sense to extract into something super reusable. Your mileage may vary!&lt;/p&gt;

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

&lt;p&gt;You've created a Tailwind CSS plugin that provides a rich set of customizable button styles. This plugin enhances your UI toolkit, enabling quick and consistent button styling across your projects.&lt;/p&gt;

&lt;p&gt;You can experiment with different styles and configurations to tailor the plugin to your design needs. Your creativity is the limit!&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/justalever/button-plugin-tailwind"&gt;Source code for this guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/docs/plugins"&gt;Tailwind CSS Plugin Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="//https:/vite.js"&gt;Vite.js docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tailwindcss</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Let’s Build: A Supplement Stack Sharing App with Ruby on Rails</title>
      <dc:creator>Andy Leverenz</dc:creator>
      <pubDate>Tue, 05 Dec 2023 17:42:49 +0000</pubDate>
      <link>https://forem.com/justalever/lets-build-a-supplement-stack-sharing-app-with-ruby-on-rails-15p6</link>
      <guid>https://forem.com/justalever/lets-build-a-supplement-stack-sharing-app-with-ruby-on-rails-15p6</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HGHWuSN9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/52qatslfpfxdd5cev7on.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HGHWuSN9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/52qatslfpfxdd5cev7on.jpg" alt="https://web-crunch.com/posts/supplement-stack-sharing-app-ruby-on-rails" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Welcome to my latest Let’s Build series, where we'll build a supplement stack sharing app using Ruby on Rails.&lt;/p&gt;

&lt;p&gt;Over time, I’ve added several &lt;a href="https://web-crunch.com/lets-builds"&gt;these builds&lt;/a&gt;, which resonate with many of my audience.&lt;/p&gt;

&lt;p&gt;These are both useful for first principles product ideas but also lovely ways to practice to see how quickly I can build an MVP of product ideas. Luckily, with Rails, this can be done quickly with &lt;a href="https://world.hey.com/dhh/the-one-person-framework-711e6318"&gt;just one developer&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Idea
&lt;/h3&gt;

&lt;p&gt;The idea for a supplement stack shopping and sharing tool (I'm dubbing Supstacker) came to me while browsing Twitter (X) one evening. I saw someone I followed to share a photo of over a dozen supplement bottles and proceeded to make a thread documenting each. While this was useful, places like X (formerly Twitter) are more of a “point in time” type of experience rather than something to refer back to often.&lt;/p&gt;

&lt;p&gt;I read the thread and quickly realized that sharing any collection of supplements you might take with friends is hard. You can go one by one, but a list over, say, five supplements and things quickly get disorganized.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plans following the build
&lt;/h3&gt;

&lt;p&gt;As a disclaimer, I like this idea and plan to make it real. I’ll share the source code with y’all for reference, but don’t copy me directly if you plan to try to do the same.&lt;/p&gt;

&lt;p&gt;When I build ideas like this, it's two-fold. To make helpful stuff people find value in and also benefit monetarily. That will be no different this time around. I suppose it’s fun to build this stuff, too, so I can’t leave that out. Do you want to build together to go faster? Reach out to me, and we can see what might make sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-requisites
&lt;/h3&gt;

&lt;p&gt;I’ll be using the latest version of Rails at the time of this writing/recording, which is Rails 7.1. We’ll leverage the &lt;a href="https://hotwired.dev/"&gt;Hotwire&lt;/a&gt; framework of frameworks and all the goodies that come with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tailwindcss.com"&gt;Tailwind CSS&lt;/a&gt; is my go-to for CSS these days, and as a bonus, I’ll make use of the alpha version of my new project &lt;a href="https://railsui.com"&gt;Rails UI&lt;/a&gt; to save some initial time on design.&lt;/p&gt;

&lt;p&gt;P.S. If you need design work tailored for Rails apps, &lt;a href="https://railsui.com/custom"&gt;I’m available for hire&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s get on with it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Video version of the series
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Part 1
&lt;/h4&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/uef6vBXyY0M"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h4&gt;
  
  
  Part 2
&lt;/h4&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/0aFehpAtdMQ"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h4&gt;
  
  
  Part 3
&lt;/h4&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/2-P40kP5f54"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h4&gt;
  
  
  Part 4
&lt;/h4&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/MCCUZW4U4ag"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h4&gt;
  
  
  Part 5
&lt;/h4&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/6dgGqKhsuFA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h4&gt;
  
  
  Part 6
&lt;/h4&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/N5Sdo26-D9E"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h4&gt;
  
  
  Part 7
&lt;/h4&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/pot-C92vKuQ"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h4&gt;
  
  
  Part 8
&lt;/h4&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/-DRQo6OVV7Q"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h4&gt;
  
  
  Part 9 - Final Part
&lt;/h4&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/sJHVtfUZ6-E"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Written version of the series
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Getting started
&lt;/h3&gt;

&lt;p&gt;I’ll start making a new Rails app on my machine using Rails 7.1 and Ruby 3.2.2. The app is a vanilla Rails app to begin with. I’m going this route since Rails UI plays more excellently alongside it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new supstacker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After scaffolding the app, install Rails UI. You can totally &lt;em&gt;not&lt;/em&gt; use &lt;a href="https://github.com/getrailsui/railsui"&gt;Rails UI&lt;/a&gt; and customize your app a bit more. I’m using it to save some configuration time and design setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add railsui &lt;span class="nt"&gt;--github&lt;/span&gt; getrailsui/railsui &lt;span class="nt"&gt;--branch&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing the gem, run.&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;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, run the Rails UI installer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails railsui:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That should fetch some dependencies and tweak the app. It also installs and configures &lt;a href="https://github.com/heartcombo/devise"&gt;Devise&lt;/a&gt; for you.&lt;/p&gt;

&lt;p&gt;Boot your app and head to the root path (localhost:3000). You can then configure your Rails UI installation by choosing an app name, support email, CSS framework, and theme for the chosen CSS framework.&lt;/p&gt;

&lt;p&gt;Saving your changes installs the framework and completes your configuration. With Rails UI installed, you now have a design system, an archive of opt-in pages, and more in your arsenal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating resources
&lt;/h2&gt;

&lt;p&gt;I’m calling this app Supstacker. It’s kinda catchy and the domain was also available💪!&lt;/p&gt;

&lt;p&gt;We’ll use the term &lt;code&gt;Stack&lt;/code&gt; as our first model, the collections of supplements. Additionally, we can use &lt;code&gt;Product&lt;/code&gt; as another model to be what gets added to a &lt;code&gt;Stack&lt;/code&gt; and ultimately shared/interacted with by your friends or public folks.&lt;/p&gt;

&lt;p&gt;Thanks to Rails UI, we already have a &lt;code&gt;User&lt;/code&gt; model, so let's build our resource list as an exercise in planning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;User&lt;/code&gt; - The model responsible for an entity who might add supplements and share stacks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Stack&lt;/code&gt; - A category or collection type of resource that acts as a basket for your supplements. Users can have many stacks. A stack has a shareable link that users can send to each other for quick and easy access.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Product&lt;/code&gt; - The singular supplement (product) shared inside a stack. This might have richer data like pricing trends, title, description, and nutrition facts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Brand&lt;/code&gt; - The brand of a supplement. A Supplement would have one brand. A brand can have many supplements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a short and sweet list to get us started.&lt;/p&gt;

&lt;p&gt;As the app build gets underway, adding additional filterable criteria and categories might be an excellent way to help people browse. Some expansion ideas include tagging, categories (protein, weight management, vitamins/wellness, etc…), brands, reviews, ingredients, and more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g scaffold Brand name description:text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g scaffold Product title:string description:text &lt;span class="nb"&gt;link&lt;/span&gt;:string asin:string &lt;span class="s1"&gt;'price:decimal{10,2}'&lt;/span&gt; brand:references
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g scaffold Stack title:string share_link:string user:references
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g migration CreateProductStacks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateProductStacks&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:product_stacks&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;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&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;
  
  
  Defining relationships
&lt;/h3&gt;

&lt;p&gt;We can move quickly thanks to ActiveRecord, which helps define relationships on the model level in Rails. I’ll list all the models below and explain what’s going on with each.&lt;/p&gt;

&lt;h4&gt;
  
  
  User Model
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;A user &lt;code&gt;has_many&lt;/code&gt; stacks.

&lt;ul&gt;
&lt;li&gt;A user should be able to make more than one stack OR perhaps with business in mind, you might offer one free stack and add a payment gateway here to upsell and earn for the service.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;A user &lt;code&gt;has_many&lt;/code&gt; products through stacks.

&lt;ul&gt;
&lt;li&gt;Being able to see all the products a user has would be awesome. With this relationship, we’re able to do so. We can query through all stacks a user has created and output the products.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/user.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:stacks&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;through: :stacks&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Stack Model
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;A stack &lt;code&gt;belongs_to&lt;/code&gt; a user.

&lt;ul&gt;
&lt;li&gt;When a user creates a &lt;code&gt;Stack,&lt;/code&gt; it references their &lt;code&gt;user_id&lt;/code&gt; even if other users can view it. This is important to easily display user profile data on a view for others to understand whose stack it actually is.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;A stack &lt;code&gt;has_many&lt;/code&gt; products.

&lt;ul&gt;
&lt;li&gt;Think of a Stack as a container of products and what we’ll allow users to share. We don’t want to limit the number of products, but requiring at least one product would make sense.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/stack.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:product_stacks&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;through&lt;/span&gt; &lt;span class="ss"&gt;:product_stacks&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Product Model
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;A product &lt;code&gt;has_many&lt;/code&gt; a &lt;code&gt;ProductStacks&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A product &lt;code&gt;has_many&lt;/code&gt; a &lt;code&gt;Stacks&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A product &lt;code&gt;belongs_to&lt;/code&gt; a &lt;code&gt;Brand&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Brands are similar to stacks but apply only to products. We’ll leverage this model to give users an easy way to filter their search for supplements and other users with products of the same brand (&lt;code&gt;brand_id&lt;/code&gt;) within their Stacks.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/product.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:brand&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:product_stacks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;dependent: :destroy&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:stacks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;through: :product_stacks&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  ProductStack model
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;ProductStack&lt;/code&gt; belongs_to a &lt;code&gt;product&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;ProductStack&lt;/code&gt; belongs_to a &lt;code&gt;stack&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;This join model allows us to retrieve products and stacks through one another more easily.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductStack&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:product&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:stack&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Brand Model
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;A brand &lt;code&gt;has_many&lt;/code&gt; products&lt;/li&gt;
&lt;li&gt;I added &lt;code&gt;dependent: :nullify&lt;/code&gt; so we can &lt;strong&gt;delete&lt;/strong&gt; a product but not necessarily the associated brand. You might see a foreign key constraint exception on the database layer if you don't do this.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/brand.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Brand&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;dependent: :nullify&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This set of relationships allows a user to have multiple stacks; each stack can contain multiple products, and each product belongs to a specific stack and brand.&lt;/p&gt;

&lt;p&gt;If you’re following along, amend your app to match the snippets above. We’ll refine the controllers and views coming up. Before we do, we need to enhance our routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial Routing
&lt;/h2&gt;

&lt;p&gt;With our resources generated, we should have a couple of new lines in the &lt;code&gt;config/routes.rb&lt;/code&gt; file that came with the app. If you're using Rails UI, your file should look similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&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;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:stacks&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:products&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:brands&lt;/span&gt;
  &lt;span class="k"&gt;if&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;development?&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test?&lt;/span&gt;
    &lt;span class="n"&gt;mount&lt;/span&gt; &lt;span class="no"&gt;Railsui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;at: &lt;/span&gt;&lt;span class="s2"&gt;"/railsui"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Inherits from Railsui::PageController#index&lt;/span&gt;
  &lt;span class="c1"&gt;# To overide, add your page#index view or change to a new root&lt;/span&gt;
  &lt;span class="c1"&gt;# Visit the start page for Rails UI any time at /railsui/start&lt;/span&gt;
  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="ss"&gt;action: :index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;controller: &lt;/span&gt;&lt;span class="s2"&gt;"railsui/page"&lt;/span&gt;

  &lt;span class="n"&gt;devise_for&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;
  &lt;span class="c1"&gt;# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html&lt;/span&gt;

  &lt;span class="c1"&gt;# Reveal health status on /up that returns 200 if the app boots with no exceptions; otherwise, 500.&lt;/span&gt;
  &lt;span class="c1"&gt;# Can be used by load balancers and uptime monitors to verify that the app is live.&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"up"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"rails/health#show"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :rails_health_check&lt;/span&gt;

  &lt;span class="c1"&gt;# Defines the root path route ("/")&lt;/span&gt;
  &lt;span class="c1"&gt;# root "posts#index"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;root&lt;/code&gt; route points to the Rails UI engine by default. Let's change this to &lt;code&gt;stacks#index&lt;/code&gt; for now.&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="c1"&gt;# config/routes.rb&lt;/span&gt;

&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"stacks#index"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Head back to &lt;code&gt;localhost:3000&lt;/code&gt;, and you should see the Rails UI theme we chose (using Tailwind CSS) and the &lt;code&gt;Stacks&lt;/code&gt; index.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--435uLX8k--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://f001.backblazeb2.com/file/webcrunch/stack-index.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--435uLX8k--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://f001.backblazeb2.com/file/webcrunch/stack-index.png" alt="Stacks index" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because we use scaffolds when generating our resources, we get the “basics” of a CRUD app, which allows you to create, read, update, and destroy each model we’ve added so far minus users.&lt;/p&gt;

&lt;p&gt;Next, let’s update the resource lines to align more with our application architecture. We’ll nest &lt;code&gt;products&lt;/code&gt; within &lt;code&gt;stacks&lt;/code&gt; .&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="c1"&gt;# config/routes.rb&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;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:brands&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:stacks&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:products&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;h2&gt;
  
  
  Clean up the scaffolds
&lt;/h2&gt;

&lt;p&gt;With scaffolds comes some unnecessary fluff that we can tighten up and remove. An example of this is the &lt;code&gt;user_id&lt;/code&gt; input on the new stack form and the &lt;code&gt;.jbuilder&lt;/code&gt; files and response types in the controllers.&lt;/p&gt;

&lt;p&gt;When done, there should be no comments in the controllers and &lt;code&gt;id&lt;/code&gt; related attributes in the views.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication strategy
&lt;/h2&gt;

&lt;p&gt;Rails UI supports Devise by default, and there are pre-designed authentication views. With that work already out of the way for this app, I’d like to require a user to be authenticated to manipulate data related to a &lt;code&gt;Stack&lt;/code&gt; . We will follow suit with the other models soon enough.&lt;/p&gt;

&lt;p&gt;Here’s my updated &lt;code&gt;stacks_controller.rb&lt;/code&gt; file.&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="c1"&gt;# app/controllers/stacks_controller.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StacksController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:authenticate_user!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;except: &lt;/span&gt;&lt;span class="sx"&gt;%i[index show]&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:set_stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="sx"&gt;%i[show edit update destroy]&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@stacks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="vi"&gt;@stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;edit&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="vi"&gt;@stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&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="n"&gt;stack_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@stack&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;redirect_to&lt;/span&gt; &lt;span class="n"&gt;stack_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Stack was successfully created."&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&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;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;stack_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Stack was successfully updated."&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&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;def&lt;/span&gt; &lt;span class="nf"&gt;destroy&lt;/span&gt;
    &lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy!&lt;/span&gt;
    &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;stacks_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Stack was successfully destroyed."&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_stack&lt;/span&gt;
      &lt;span class="vi"&gt;@stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stack_params&lt;/span&gt;
      &lt;span class="n"&gt;params&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="ss"&gt;:stack&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Besides cleaning the file, I added a before_action called &lt;code&gt;:authenticate_user&lt;/code&gt;. This is built into the Devise gem and requires a session to bypass. If a user isn’t logged in, they are redirected to the form to do so. You can declare which actions in the controller this applies to. In this case, we want it to apply to everything except the index and show routes.&lt;/p&gt;

&lt;p&gt;On top of the new before action, I added some extended logic to assign the current user to a stack inside the &lt;code&gt;create&lt;/code&gt; action. That way, we will know who created it based on their &lt;code&gt;user_id.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So it's extra evident the &lt;code&gt;user_id&lt;/code&gt; field was generated previously when we first developed the &lt;code&gt;Stack&lt;/code&gt; resource. You can check your &lt;code&gt;schema.rb&lt;/code&gt; file to see it on the &lt;code&gt;stacks&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;The line &lt;code&gt;user:references&lt;/code&gt; worked for us on the generator command.&lt;/p&gt;

&lt;p&gt;Here’s an excerpt from my schema file for reference.&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="n"&gt;create_table&lt;/span&gt; &lt;span class="s2"&gt;"stacks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;force: :cascade&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;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="s2"&gt;"title"&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt; &lt;span class="s2"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt; &lt;span class="s2"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt; &lt;span class="s2"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"index_stacks_on_user_id"&lt;/span&gt;  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you haven’t already, sign up for an account at &lt;code&gt;localhost:3000/users/sign_up&lt;/code&gt; . Because we are working locally, you can use a fake email and password. I'd suggest something super easy to remember, for speed's sake.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try creating a Stack
&lt;/h3&gt;

&lt;p&gt;Right now, a Stack by itself is underwhelming. Let’s create a stack and ensure the user data saves correctly. Then, we can proceed to the &lt;code&gt;Product&lt;/code&gt; model.&lt;/p&gt;

&lt;p&gt;I created one called &lt;code&gt;Andy’s Stack,&lt;/code&gt; and it worked!&lt;/p&gt;

&lt;p&gt;To verify it saved my user data, we can leverage &lt;code&gt;rails console&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails console
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then type&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That should then output our first stack&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;irb&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&lt;/span&gt;:001&amp;gt; Stack.last
  Stack Load &lt;span class="o"&gt;(&lt;/span&gt;0.1ms&lt;span class="o"&gt;)&lt;/span&gt; SELECT &lt;span class="s2"&gt;"stacks"&lt;/span&gt;.&lt;span class="k"&gt;*&lt;/span&gt; FROM &lt;span class="s2"&gt;"stacks"&lt;/span&gt; ORDER BY &lt;span class="s2"&gt;"stacks"&lt;/span&gt;.&lt;span class="s2"&gt;"id"&lt;/span&gt; DESC LIMIT ? &lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="s2"&gt;"LIMIT"&lt;/span&gt;, 1]]
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;#&amp;lt;Stack:0x00000001064f4c58&lt;/span&gt;
 &lt;span class="nb"&gt;id&lt;/span&gt;: 1,
 title: &lt;span class="s2"&gt;"Andy's Stack"&lt;/span&gt;,
 user_id: 1,
 created_at: Mon, 27 Nov 2023 21:25:35.307793000 UTC +00:00,
 updated_at: Mon, 27 Nov 2023 21:25:35.307793000 UTC +00:00&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;user_id&lt;/code&gt; column has the value &lt;code&gt;1&lt;/code&gt;. This gives us a clue that our logic is working!&lt;/p&gt;

&lt;p&gt;For bonus points, you could write if you wanted to see information about the user assigned to the stack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Stack&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;user&lt;/span&gt;
 &lt;span class="no"&gt;Stack&lt;/span&gt; &lt;span class="no"&gt;Load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="s2"&gt;"stacks"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;*&lt;/span&gt; &lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="s2"&gt;"stacks"&lt;/span&gt; &lt;span class="no"&gt;ORDER&lt;/span&gt; &lt;span class="no"&gt;BY&lt;/span&gt; &lt;span class="s2"&gt;"stacks"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt; &lt;span class="no"&gt;DESC&lt;/span&gt; &lt;span class="no"&gt;LIMIT&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="s2"&gt;"LIMIT"&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="no"&gt;User&lt;/span&gt; &lt;span class="no"&gt;Load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="s2"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;*&lt;/span&gt; &lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="s2"&gt;"users"&lt;/span&gt; &lt;span class="no"&gt;WHERE&lt;/span&gt; &lt;span class="s2"&gt;"users"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="no"&gt;LIMIT&lt;/span&gt; &lt;span class="p"&gt;?&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"LIMIT"&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;#&amp;lt;User id: 1, email: "andy@example.com", first_name: nil, last_name: nil, admin: false, created_at: "2023-11-27 21:19:52.662926000 +0000", updated_at: "2023-11-27 21:19:52.662926000 +0000"&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works fine now, but it won’t be as accurate as your data scales. You might need to be more explicit. For example:&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="n"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;340&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Products
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Product&lt;/code&gt; model is where a single supplement resides. A product should be able to be a part of any user's stack. We’ll want to make sure a product doesn't already exist in the database before adding it.&lt;/p&gt;

&lt;p&gt;My goal for this Let’s Build series is to append the attributes below dynamically. We’ll use a bit of web scrapping to do this along with &lt;a href="https://nokogiri.org/"&gt;Nokogiri&lt;/a&gt;, a super cool ruby gem. A user needs to supply a link to the product page on Amazon, and the app will parse the rest to the best of its ability.&lt;/p&gt;

&lt;p&gt;We’ll add more granular details for the product, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;asin&lt;/code&gt; (&lt;a href="https://en.wikipedia.org/wiki/Amazon_Standard_Identification_Number"&gt;amazon standard identification number&lt;/a&gt;)

&lt;ul&gt;
&lt;li&gt;I chose Amazon since it’s the most prominent place to shop (in the States). You could integrate multiple merchants eventually and include various links to those. If you were to do various merchants, it might make sense to make a new model so it’s additional criteria a user could filter products/stacks by.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;description&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Brief description of the product. This might include ingredients or something.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;link&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;A link where the user purchased it originally&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;price&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;The original purchase price as a decimal&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;brand_id&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;We can provide more accessible filtering logic with the &lt;code&gt;Brand&lt;/code&gt; model coming up so users can find products and stacks that might match their preferences.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Authentication strategy for products
&lt;/h2&gt;

&lt;p&gt;Much like stacks, I want to require a user session to manipulate the data related to a product. We’ll add more logic to allow &lt;strong&gt;only&lt;/strong&gt; the user who created the product in the first place permissions to do so.&lt;/p&gt;

&lt;p&gt;Here’s the updated controller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:authenticate_user!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;except: &lt;/span&gt;&lt;span class="sx"&gt;%i[index show]&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:set_product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="sx"&gt;%i[show edit update destroy]&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="vi"&gt;@product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;edit&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="vi"&gt;@product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&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="n"&gt;product_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@product&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;redirect_to&lt;/span&gt; &lt;span class="n"&gt;product_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Product was successfully created."&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&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;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;product_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Product was successfully updated."&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&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;def&lt;/span&gt; &lt;span class="nf"&gt;destroy&lt;/span&gt;
    &lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy!&lt;/span&gt;

    &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;products_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Product was successfully destroyed."&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_product&lt;/span&gt;
      &lt;span class="vi"&gt;@product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;product_params&lt;/span&gt;
      &lt;span class="n"&gt;params&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="ss"&gt;:product&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&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="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:asin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:brand_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Besides cleaning up the logic from the scaffold, we added a new line right after the class declaration &lt;code&gt;authenticate_user!&lt;/code&gt; Much like the &lt;code&gt;stacks_controller.rb&lt;/code&gt; file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update the product form
&lt;/h3&gt;

&lt;p&gt;I want the “Add product” form to be straightforward. You pass a URL to the product on Amazon, and the app goes and fetches data and creates the product. Our form becomes quite simple then.&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="c"&gt;&amp;lt;!-- app/views/products/_form.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;form_with&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;model:&lt;/span&gt; &lt;span class="err"&gt;[@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="err"&gt;])&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;form&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt;
&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;shared&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;error_messages&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;resource:&lt;/span&gt; &lt;span class="na"&gt;form.object&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-normal text-lg mb-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Provide the link to the Amazon product, and we'll do the rest.
&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-group"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;form.label&lt;/span&gt; &lt;span class="na"&gt;:link&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Amazon&lt;/span&gt; &lt;span class="na"&gt;link&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;form-label&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt;
  &lt;span class="na"&gt;form.text_field&lt;/span&gt; &lt;span class="na"&gt;:link&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;form-input&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;placeholder:&lt;/span&gt;
  &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;https:&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="na"&gt;www.amazon.com&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;OPTIMUM-NUTRITION-STANDARD-Naturally-Flavored&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;dp&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;B00QQA0H3S&lt;/span&gt;&lt;span class="err"&gt;/"&lt;/span&gt;
  &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"prose prose-sm max-w-full pt-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
      Example link:
      &lt;span class="nt"&gt;&amp;lt;code&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;https://www.amazon.com/OPTIMUM-NUTRITION-STANDARD-Naturally-Flavored/dp/B00QQA0H3S/&lt;span class="nt"&gt;&amp;lt;/code&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;form.submit&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Add&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;btn&lt;/span&gt; &lt;span class="na"&gt;btn-primary&lt;/span&gt; &lt;span class="na"&gt;btn-lg&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Editing a product
&lt;/h2&gt;

&lt;p&gt;Even though products will be created dynamically, we want to give users the ability to change the details as necessary. I’ll make a new form partial in &lt;code&gt;app/views/products&lt;/code&gt; called &lt;code&gt;_edit_form.html.erb&lt;/code&gt; .&lt;/p&gt;

&lt;p&gt;This form will have all the criteria of a product&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;%= form_with(model: [@stack, product]) do |form| %&amp;gt; &amp;lt;%= render
"shared/error_messages", resource: form.object %&amp;gt;

&amp;lt;h3 class="font-normal text-lg mb-6"&amp;gt;
  Provide the link to the Amazon product and we'll do the rest.
&amp;lt;/h3&amp;gt;

&amp;lt;div class="form-group"&amp;gt;
  &amp;lt;%= form.label :thumbnail, class: "form-label" %&amp;gt; &amp;lt;%= form.file_field
  :thumbnail, class: "form-file-input" %&amp;gt; &amp;lt;% if form.object.thumbnail.attached?
  %&amp;gt;
  &amp;lt;div class="my-2"&amp;gt;
    &amp;lt;%= image_tag product.thumbnail, class: "w-32 h-auto" %&amp;gt; &amp;lt;%= link_to "Remove
    Thumbnail", remove_thumbnail_stack_product_path(@stack, product), class:
    "btn btn-link" %&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;% end %&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;div class="form-group"&amp;gt;
  &amp;lt;%= form.label :title, class: "form-label" %&amp;gt; &amp;lt;%= form.text_field :title,
  class: "form-input" %&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;div class="form-group"&amp;gt;
  &amp;lt;%= form.label :price, class: "form-label" %&amp;gt; &amp;lt;%= form.number_field :price,
  step: "0.01", class: "form-input" %&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;div class="form-group"&amp;gt;
  &amp;lt;%= form.label :description, class: "form-label" %&amp;gt; &amp;lt;%= form.text_area
  :description, class: "form-input min-h-[220px]" %&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;div class="form-group"&amp;gt;
  &amp;lt;%= form.label :asin, class: "form-label" %&amp;gt; &amp;lt;%= form.text_field :asin, class:
  "form-input" %&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;div class="form-group"&amp;gt;
  &amp;lt;%= form.label :brand_id, "Brand", class: "form-label" %&amp;gt; &amp;lt;%=
  form.collection_select :brand_id, Brand.all, :id, :name, { prompt: "Select
  one" }, {class: "form-select"} %&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;div class="form-group"&amp;gt;
  &amp;lt;%= form.label :link, "Amazon link", class: "form-label" %&amp;gt; &amp;lt;%=
  form.text_field :link, class: "form-input", placeholder:
  "https://www.amazon.com/OPTIMUM-NUTRITION-STANDARD-Naturally-Flavored/dp/B00QQA0H3S/"
  %&amp;gt;
  &amp;lt;div class="prose prose-sm max-w-full pt-3"&amp;gt;
    &amp;lt;p&amp;gt;
      Example link:
      &amp;lt;code
        &amp;gt;https://www.amazon.com/OPTIMUM-NUTRITION-STANDARD-Naturally-Flavored/dp/B00QQA0H3S/&amp;lt;/code
      &amp;gt;
    &amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;%= form.submit "Add product", class: "btn btn-primary btn-lg" %&amp;gt; 
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Removing a product thumbnail
&lt;/h2&gt;

&lt;p&gt;For exercise, I added a link to remove an attachment if it’s present on a product. We first check for its existence and display it if it does exist. The link to remove it is a new endpoint added to our controller and routing to simplify it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb &lt;/span&gt;
&lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:stacks&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; 
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:products&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; 
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'remove_thumbnail'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;on: :member&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;Here’s my controller after updating to match the new routing structure and additional end point to remove a thumbnail.&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="c1"&gt;# app/controllers/products_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:authenticate_user!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;except: &lt;/span&gt;&lt;span class="sx"&gt;%i[index show]&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:set_product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="sx"&gt;%i[show edit update destroy remove_thumbnail]&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:set_stack&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="vi"&gt;@product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;edit&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;stack_product_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Product was successfully updated."&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&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;def&lt;/span&gt; &lt;span class="nf"&gt;remove_thumbnail&lt;/span&gt;
    &lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;thumbnail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;purge&lt;/span&gt;

    &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;stack_product_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s1"&gt;'Thumbnail removed successfully.'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;destroy&lt;/span&gt;
    &lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy!&lt;/span&gt;

    &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;stack_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Product was successfully destroyed."&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_stack&lt;/span&gt;
      &lt;span class="vi"&gt;@stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:stack_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_product&lt;/span&gt;
      &lt;span class="vi"&gt;@product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;product_params&lt;/span&gt;
      &lt;span class="n"&gt;params&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="ss"&gt;:product&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&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="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:asin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:brand_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ActiveStorage has a built-in &lt;code&gt;purge&lt;/code&gt; method we can leverage to remove the attachment easily. I'll do a simple redirect back to the product after removing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a product dynamically
&lt;/h2&gt;

&lt;p&gt;I don’t believe I’ve covered web scraping this blog before. It’s a bit of a brittle way to get data from other sites for your site. Most websites frown on scraping data, so use this at your discretion. If you get in trouble, neither Web-Crunch.com nor I can be held responsible.&lt;/p&gt;

&lt;p&gt;With that disclaimer out of the way, let’s create a new concern we can use throughout the app. This type of thing is a little taxing, so I want to make a separate module to handle the logic and a background job for queuing up the processes as they transpire. Putting this in a queue frees up your app’s resources and allows the end user to continue navigating without waiting. All that being said, since the process takes a little time, a user needs to be told what’s happening while they wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing data automatically
&lt;/h2&gt;

&lt;p&gt;We’ll make products a bit more dynamic with the help of Nokogiri. I want to capture a thumbnail of the product, among other things, once it’s created, if possible. We can inspect elements on a given product page and look for unique class names or IDs. Ideally, they are present for easier access via Nokogiri.&lt;/p&gt;

&lt;p&gt;For now, to prototype this, let’s make a new ruby module (a.k.a. a concern) inside the &lt;code&gt;app/models/concerns&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Below is the basic module structure. You could extend this to include new sources other than Amazon, depending on your needs and sources.&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="c1"&gt;# app/models/concerns/product_parser.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ProductParser&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Amazon&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;
  
  
  User agents
&lt;/h3&gt;

&lt;p&gt;We need to randomize our user agent when accessing the URL to get past some of Amazon's guards for another type of scrapper. With the help of ChatGPT and Google, I found a large array we can utilize.&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="c1"&gt;# app/models/concerns/product_parser.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ProductParser&lt;/span&gt;
  &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'open-uri'&lt;/span&gt;

  &lt;span class="no"&gt;COMMON_USER_AGENTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;URI&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'User-Agent'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;COMMON_USER_AGENTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Nokogiri&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&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;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Parsing Logic 101
&lt;/h2&gt;

&lt;p&gt;That file turns into something like this with an internal &lt;code&gt;parse_doc&lt;/code&gt; method. It accepts a URL. With the help of the &lt;code&gt;open-uri&lt;/code&gt; library in Ruby, we can fetch it as a random user agent. Then, loop through the hash returned as parse the HTML using Nokogiri.&lt;/p&gt;

&lt;p&gt;Think of this process as Ruby/Nokogiri visiting the URL and scanning the DOM elements of the page. From there, we traverse a little deeper to find specific aspects of the data we are after.&lt;/p&gt;

&lt;p&gt;This process is, without a doubt, error-prone and brittle. You’re at the mercy of Amazon updating their HTML or CSS classes, so a routine check to ensure the page is scraping correctly would be wise. Tests can solve this, which I’ll do coming up.&lt;/p&gt;

&lt;p&gt;If they aren't present, I’ll update the parse with elements and conditional logic. Additionally, the module will create a new product using the attributes we save. Here’s my final parser file.&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="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ActionView&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Helpers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SanitizeHelper&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ProductParser&lt;/span&gt;
  &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'open-uri'&lt;/span&gt;

  &lt;span class="no"&gt;COMMON_USER_AGENTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;URI&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'User-Agent'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;COMMON_USER_AGENTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Nokogiri&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&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;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Amazon&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
      &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ProductParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="c1"&gt;# title&lt;/span&gt;
      &lt;span class="n"&gt;title_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#productTitle'&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;title_element&lt;/span&gt;
        &lt;span class="n"&gt;title_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title_string&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="c1"&gt;# description&lt;/span&gt;
      &lt;span class="n"&gt;description_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#productDescription'&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;description_element&lt;/span&gt;
        &lt;span class="n"&gt;dirty_description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;description_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_html&lt;/span&gt;
        &lt;span class="n"&gt;clean_description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dirty_description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;tags: &lt;/span&gt;&lt;span class="sx"&gt;%w[h3 span p]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clean_description&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="c1"&gt;# price&lt;/span&gt;
      &lt;span class="n"&gt;price_1_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#corePrice_feature_div .a-offscreen'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;price_2_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.header-price span'&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;price_1_element&lt;/span&gt;
        &lt;span class="n"&gt;price_1_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price_1_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_html&lt;/span&gt;
        &lt;span class="n"&gt;price_1_decimal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price_1_string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/[^0-9.]/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_d&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price_1_decimal&lt;/span&gt;
      &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;price_2_element&lt;/span&gt;
        &lt;span class="n"&gt;price_2_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price_2_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_html&lt;/span&gt;
        &lt;span class="n"&gt;price_2_decimal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price_2_string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/[^0-9.]/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_d&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price_2_decimal&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="c1"&gt;# brand&lt;/span&gt;
      &lt;span class="n"&gt;brand_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.po-brand .a-span9 span'&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;brand_element&lt;/span&gt;
        &lt;span class="n"&gt;brand_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;brand_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_html&lt;/span&gt;
        &lt;span class="n"&gt;brand_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;brand_string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
        &lt;span class="n"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Brand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_or_create_by!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;brand_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:brand&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;brand&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="c1"&gt;# thumbnail&lt;/span&gt;
      &lt;span class="n"&gt;thumbnail_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#landingImage'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;thumbnail_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;thumbnail_element&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'src'&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;thumbnail_element&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;thumbnail_url&lt;/span&gt;
        &lt;span class="n"&gt;thumbnail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&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;thumbnail_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:thumbnail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ActiveStorage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_and_upload!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;io: &lt;/span&gt;&lt;span class="n"&gt;thumbnail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;filename: &lt;/span&gt;&lt;span class="s2"&gt;"thumbnail_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="c1"&gt;# asin&lt;/span&gt;
      &lt;span class="n"&gt;asin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;%r{dp/([^/]+)/}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;captures&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
      &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:asin&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asin&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_to_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;begin&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&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="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Error in save_to_product: &lt;/span&gt;&lt;span class="si"&gt;#{&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;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&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;backtrace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="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;p&gt;In this file, we find and collect the following bits of data. It’s stored in an &lt;code&gt;attributes&lt;/code&gt; variable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;price_1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;price_2&lt;/code&gt; - A backup price on different versions of the product page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;brand&lt;/code&gt; - Starts as a string, and we take that string and either create a new product or find an existing one in our database to assign.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;thumbnail&lt;/code&gt; - Pull the thumbnail from the doc and attach it via ActiveStorage.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;asin&lt;/code&gt; - We do some Ruby magic and use a Regular expression pattern to extract the ASIN from the url.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far, we’ve only parsed the data. We haven’t saved a new product. We’ll do that coming up in our ProductsController and a background job. For now, I’ll note the method in the parser called &lt;code&gt;save_to_product&lt;/code&gt; that accepts the &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;product&lt;/code&gt; instances. This will make more sense in a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a product for real
&lt;/h2&gt;

&lt;p&gt;If you remember our new product form, you will recall it’s a simple field and a submit button. The end user adds a link from Amazon to the field and clicks Create Product. That brings us to our next step. By design/convention, the form on &lt;code&gt;/stacks/:stack_id/products/new&lt;/code&gt; will leverage a &lt;code&gt;POST&lt;/code&gt; HTTP response to our server. Doing so sends a request with the &lt;code&gt;link&lt;/code&gt; parameter. We'll take that and amend our &lt;code&gt;create&lt;/code&gt; action in the controller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="c1"&gt;#...&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:product&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="ss"&gt;:link&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
    &lt;span class="no"&gt;ProductImportJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@stack&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="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;stack_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Product is being created, sit tight!"&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;Here we go “anti” conventional a bit and introduce a few new steps in the chain to create a product dynamically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a product import background job
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;ProductImportJob&lt;/code&gt; class is in the &lt;code&gt;create&lt;/code&gt; action. Let's create that quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g job ProductImport
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes a new file &lt;code&gt;app/jobs/product_import_job.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In this file, we’ll leverage the new parser to put the taxing processes in a queuing service, which in return won’t tax our app’s resources and also gives the end user a better experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add and configure sidekiq
&lt;/h3&gt;

&lt;p&gt;I prefer to use Sidekiq for my background queuing service. There are a few configuration steps to get out of the way before we do.&lt;/p&gt;

&lt;p&gt;Add &lt;a href="https://github.com/sidekiq/sidekiq/wiki/Active-Job"&gt;sidekiq&lt;/a&gt; to your &lt;code&gt;Gemfile&lt;/code&gt; or run the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add sidekiq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Head to &lt;code&gt;config/application.rb&lt;/code&gt; and add the following to your app. Here's my file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s2"&gt;"boot"&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"rails/all"&lt;/span&gt;

&lt;span class="c1"&gt;# Require the gems listed in Gemfile, including any gems&lt;/span&gt;
&lt;span class="c1"&gt;# you've limited to :test, :development, or :production.&lt;/span&gt;
&lt;span class="no"&gt;Bundler&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="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;groups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Supstacker&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Application&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generators&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;g&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;template_engine&lt;/span&gt; &lt;span class="ss"&gt;:railsui&lt;/span&gt;
      &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fallbacks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:railsui&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:erb&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_prepare&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="no"&gt;Devise&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Mailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;layout&lt;/span&gt; &lt;span class="s2"&gt;"mailer"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:sidekiq&lt;/span&gt; &lt;span class="c1"&gt;# ADD THIS LINE&lt;/span&gt;

    &lt;span class="c1"&gt;# Initialize configuration defaults for originally generated Rails version.&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_defaults&lt;/span&gt; &lt;span class="mf"&gt;7.1&lt;/span&gt;

    &lt;span class="c1"&gt;# Please, add to the `ignore` list any other `lib` subdirectories that do&lt;/span&gt;
    &lt;span class="c1"&gt;# not contain `.rb` files, or that should not be reloaded or eager loaded.&lt;/span&gt;
    &lt;span class="c1"&gt;# Common ones are `templates`, `generators`, or `middleware`, for example.&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;autoload_lib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;ignore: &lt;/span&gt;&lt;span class="sx"&gt;%w(assets tasks)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Configuration for the application, engines, and railties goes here.&lt;/span&gt;
    &lt;span class="c1"&gt;#&lt;/span&gt;
    &lt;span class="c1"&gt;# These settings can be overridden in specific environments using the files&lt;/span&gt;
    &lt;span class="c1"&gt;# in config/environments, which are processed later.&lt;/span&gt;
    &lt;span class="c1"&gt;#&lt;/span&gt;
    &lt;span class="c1"&gt;# config.time_zone = "Central Time (US &amp;amp; Canada)"&lt;/span&gt;
    &lt;span class="c1"&gt;# config.eager_load_paths &amp;lt;&amp;lt; Rails.root.join("extras")&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;If you have a Procfile.dev file, add the following lines (with Rails UI, you should have this).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Procfile.dev&lt;/span&gt;
web: bin/rails server &lt;span class="nt"&gt;-p&lt;/span&gt; 3000
js: yarn build &lt;span class="nt"&gt;--watch&lt;/span&gt;
css: yarn build:css &lt;span class="nt"&gt;--watch&lt;/span&gt;
worker: bundle &lt;span class="nb"&gt;exec &lt;/span&gt;sidekiq &lt;span class="c"&gt;# add this one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, I like to use the UI reasonably often, so I added the following to my routes file.&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="c1"&gt;# config/routes.rb&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;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;defined?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"sidekiq/web"&lt;/span&gt;
    &lt;span class="n"&gt;mount&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Web&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"/sidekiq"&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;Restart your server &lt;code&gt;bin/dev&lt;/code&gt; with that out of the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the product job import logic
&lt;/h2&gt;

&lt;p&gt;Our logic in the job is pretty simple.&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="c1"&gt;# app/jobs/product_import_job.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductImportJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;

&lt;span class="c1"&gt;# after_perform do |job|&lt;/span&gt;
    &lt;span class="c1"&gt;# Send notification if you want&lt;/span&gt;
&lt;span class="c1"&gt;# end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stack_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Check if the product already exists based on the link&lt;/span&gt;
    &lt;span class="n"&gt;existing_product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;link: &lt;/span&gt;&lt;span class="n"&gt;link&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;existing_product&lt;/span&gt;
      &lt;span class="c1"&gt;# If the product exists, associate it with the stack&lt;/span&gt;
      &lt;span class="n"&gt;existing_product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stacks&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="c1"&gt;# If the product doesn't exist, create a new one&lt;/span&gt;
      &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&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;link: &lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stacks&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;
      &lt;span class="no"&gt;ProductParser&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Amazon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_to_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When performing the job, we pass the link and the &lt;code&gt;stack_id&lt;/code&gt; as arguments. Because products can belong to other stacks, it makes sense to rinse and reuse those if we have them in the database. Instead of creating a duplicate product, we’ll assign the existing one to the respective stack.&lt;/p&gt;

&lt;p&gt;See the parser at work? If the product doesn’t exist, we offload the work to the parser and pass the link and product to it using the &lt;code&gt;save_to_product&lt;/code&gt; method I called attention to earlier.&lt;/p&gt;

&lt;p&gt;That then fetches all the attributes, assigns them to the product instance, and saves the product to the database.&lt;/p&gt;

&lt;p&gt;After completing the job, you could use fancy logic to refresh the page with ActionCable. I left this out for the sake of time but invite you to extend it. Note the comment in the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductImportJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
&lt;span class="c1"&gt;# after_perform do |job|&lt;/span&gt;
    &lt;span class="c1"&gt;# Send notification if you want&lt;/span&gt;
&lt;span class="c1"&gt;# end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This callback method gets called where you can do something further. I’ll often send emails or trigger notifications here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify with a test
&lt;/h2&gt;

&lt;p&gt;To actively test our parser, we should add a test to ensure it returns data. This will help automate our scrapping logic so we know if it fails, we can make more immediate improvements. Amazon will no doubt update its product pages. It’s not a matter of if but when.&lt;/p&gt;

&lt;p&gt;Before we add the test, we need to update our &lt;code&gt;users.yml&lt;/code&gt; fixture file. This is so we can leverage devise in our tests. You can add whatever dummy content you want here. I said the following.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# test/fixtures/users.yml&lt;/span&gt;
&lt;span class="na"&gt;one&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;first_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;John&lt;/span&gt;
  &lt;span class="na"&gt;last_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Doe&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;john@example.com&lt;/span&gt;
&lt;span class="na"&gt;two&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;first_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Jane&lt;/span&gt;
  &lt;span class="na"&gt;last_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Doe&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jane@example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’ll just put a new test in &lt;code&gt;test/&lt;/code&gt; directly called &lt;code&gt;product_parser_test.rb&lt;/code&gt; for the parser test. Here's my file.&lt;br&gt;&lt;br&gt;
We require a logged-in user to add a product, so we need to use the devise &lt;code&gt;sign_in&lt;/code&gt; to test an authenticated user. Be sure to include the line &lt;code&gt;include Devise::Test::IntegrationHelpers&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"test_helper"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductParserTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TestCase&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ProductParser&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Devise&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;IntegrationHelpers&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;
    &lt;span class="n"&gt;sign_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:one&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@amazon_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://www.amazon.com/BSN-XPLODE-Pre-Workout-Supplement-Beta-Alanine/dp/B007XRVL2Y/"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_parse_doc&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ProductParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@amazon_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assert_instance_of&lt;/span&gt; &lt;span class="no"&gt;Nokogiri&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_amazon_get_attributes&lt;/span&gt;
    &lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ProductParser&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Amazon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@amazon_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;assert_not_nil&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assert_not_nil&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assert_not_nil&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assert_not_nil&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:brand&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assert_not_nil&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:thumbnail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assert_not_nil&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:asin&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can try it out by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails &lt;span class="nb"&gt;test test&lt;/span&gt;/product_parser_test.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all goes well, you’ll see two green tests. If not, check the logs and debug :)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails &lt;span class="nb"&gt;test test&lt;/span&gt;/product_parser_test.rb

Running 2 tests &lt;span class="k"&gt;in &lt;/span&gt;a single process &lt;span class="o"&gt;(&lt;/span&gt;parallelization threshold is 50&lt;span class="o"&gt;)&lt;/span&gt;
Run options: &lt;span class="nt"&gt;--seed&lt;/span&gt; 36475

&lt;span class="c"&gt;# Running:&lt;/span&gt;
...

Finished &lt;span class="k"&gt;in &lt;/span&gt;3.899943s, 0.5128 runs/s, 1.7949 assertions/s.
2 runs, 7 assertions, 0 failures, 0 errors, 0 skips
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trying it all out
&lt;/h2&gt;

&lt;p&gt;Here’s a quick video of me running through the process on my local machine. One thing to note is that once you submit the form, there’s no automatic page refresh to show the new product.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://share.cleanshot.com/1gc8jz5Q"&gt;View the demo&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Automatically refresh the project list with Turbo streams
&lt;/h1&gt;

&lt;p&gt;I wasn’t going to include this due to time originally, but it’s driving me up the wall, so let’s have a Stack list of products update automatically when the product import job is complete.&lt;/p&gt;

&lt;h4&gt;
  
  
  Stack Show view
&lt;/h4&gt;

&lt;p&gt;I’ve updated my views a bit to help make turbo logic easier. Instead of rendering a new form over on &lt;code&gt;/stack/1/products/new&lt;/code&gt;, I decided to embed that form right on the Stack show view to simplify things. Here is the updates stack show view:&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="c"&gt;&amp;lt;!-- app/views/stacks/show.html.erb--&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl mx-auto px-4 my-16"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pb-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;nav&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"breadcrumb"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-6 font-medium flex text-slate-500 dark:text-slate-200 text-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ol&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-wrap items-center space-x-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Stacks&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;stacks_path&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;hover:underline&lt;/span&gt; &lt;span class="na"&gt;hover:text-slate-600&lt;/span&gt; &lt;span class="na"&gt;dark:hover:text-slate-400&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex space-x-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-300 dark:text-slate-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;/&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-indigo-600 dark:text-indigo-500"&lt;/span&gt; &lt;span class="na"&gt;aria-current=&lt;/span&gt;&lt;span class="s"&gt;"page"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack.title&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h3 mb-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack.title&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &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;"flex items-center gap-4 py-3 px-2 border-y"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 flex items-center gap-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;time&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-600 dark:text-slate-400 text-xs"&lt;/span&gt; &lt;span class="na"&gt;datetime=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;%= @stack.created_at.to_formatted_s(:long) %&amp;gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Created &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;time_ago_in_words&lt;/span&gt;&lt;span class="err"&gt;(@&lt;/span&gt;&lt;span class="na"&gt;stack.created_at&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="na"&gt;ago&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/time&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt; &lt;span class="na"&gt;current_user_stack&lt;/span&gt;&lt;span class="err"&gt;?(@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Edit&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;edit_stack_path&lt;/span&gt;&lt;span class="err"&gt;(@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;),&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;btn&lt;/span&gt; &lt;span class="na"&gt;btn-light&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;else&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Create&lt;/span&gt; &lt;span class="na"&gt;your&lt;/span&gt; &lt;span class="na"&gt;own&lt;/span&gt; &lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;new_user_registration_path&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;btn&lt;/span&gt; &lt;span class="na"&gt;btn-primary&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"p-6 border bg-indigo-50/20 border-indigo-600/50 rounded-2xl shadow-sm mb-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-semibold text-xl tracking-tight mb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add a product&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;form&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;product:&lt;/span&gt; &lt;span class="na"&gt;Product.new&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;products_count_#&lt;/span&gt;&lt;span class="err"&gt;{@&lt;/span&gt;&lt;span class="na"&gt;stack.id&lt;/span&gt;&lt;span class="err"&gt;}"&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;stack_product_count&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;stack:&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;product_list_#&lt;/span&gt;&lt;span class="err"&gt;{@&lt;/span&gt;&lt;span class="na"&gt;stack.id&lt;/span&gt;&lt;span class="err"&gt;}"&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;product_list&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;stack:&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Towards the bottom, we render the new product form. Below that is the magic that makes turbo stream updates to our view.&lt;/p&gt;

&lt;p&gt;First, we need this line:&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;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one-liner is responsible for listening to websocket updates as they transpire. We’ll add some logic to our background job to “trigger” the upcoming update.&lt;/p&gt;

&lt;p&gt;Below that line is our new product list, which I extracted to a partial.&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;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;product_list_#&lt;/span&gt;&lt;span class="err"&gt;{@&lt;/span&gt;&lt;span class="na"&gt;stack.id&lt;/span&gt;&lt;span class="err"&gt;}"&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;product_list&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;stack:&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That partial is located in &lt;code&gt;app/stacks/product_list.html.erb&lt;/code&gt;&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="c"&gt;&amp;lt;!-- app/stacks/product_list.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;stack.products.each&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;product_slim&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;stack:&lt;/span&gt; &lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;product:&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, inside the &lt;code&gt;products/product_slim.html.erb&lt;/code&gt; partial is the slender details of a product.&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="c"&gt;&amp;lt;!-- app/views/products/_product_slim.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-3 flex items-center gap-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt; &lt;span class="na"&gt;product.thumbnail.attached&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="na"&gt;stack_product_path&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="err"&gt;),&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;image_tag&lt;/span&gt; &lt;span class="na"&gt;url_for&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;product.thumbnail&lt;/span&gt;&lt;span class="err"&gt;),&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;w-12&lt;/span&gt; &lt;span class="na"&gt;h-12&lt;/span&gt; &lt;span class="na"&gt;p-1&lt;/span&gt; &lt;span class="na"&gt;object-contain&lt;/span&gt; &lt;span class="na"&gt;border&lt;/span&gt; &lt;span class="na"&gt;rounded&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;alt:&lt;/span&gt; &lt;span class="na"&gt;product.title&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-start justify-between gap-4 flex-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="na"&gt;stack_product_path&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="err"&gt;),&lt;/span&gt; &lt;span class="na"&gt;target:&lt;/span&gt; &lt;span class="na"&gt;:_top&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h4&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-normal text-sm group-hover:text-indigo-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;truncate&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;product.title&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;length:&lt;/span&gt; &lt;span class="err"&gt;75)&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/h4&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-medium text-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;number_to_currency&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;product.price&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-end"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Buy&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt; &lt;span class="na"&gt;Amazon&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;product.link&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target:&lt;/span&gt; &lt;span class="na"&gt;:_blank&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;btn&lt;/span&gt; &lt;span class="na"&gt;btn-dark&lt;/span&gt; &lt;span class="na"&gt;btn-sm&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;target:&lt;/span&gt; &lt;span class="na"&gt;:_top&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, one more for brownie points is partial to update the count in real time with turbo, too.&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="c"&gt;&amp;lt;!-- app/stacks/_stack_product_count.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pt-6 h4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;pluralize&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;stack.products.size&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="na"&gt;Product&lt;/span&gt;&lt;span class="err"&gt;')&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Product show
&lt;/h2&gt;

&lt;p&gt;The product shown is another page showing more details of the product. It’s nothing special, but it gets the job done for now. Expand on the design as you want.&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="c"&gt;&amp;lt;!-- app/views/product/show.html.erb--&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl mx-auto px-4 my-16"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pb-6 border-b"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;nav&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"breadcrumb"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-6 font-medium flex text-slate-500 dark:text-slate-200 text-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ol&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-wrap items-center space-x-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack.title&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stack_path&lt;/span&gt;&lt;span class="err"&gt;(@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;),&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;hover:underline&lt;/span&gt; &lt;span class="na"&gt;hover:text-slate-600&lt;/span&gt; &lt;span class="na"&gt;dark:hover:text-slate-400&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex space-x-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-300 dark:text-slate-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;/&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-indigo-600 dark:text-indigo-500"&lt;/span&gt; &lt;span class="na"&gt;aria-current=&lt;/span&gt;&lt;span class="s"&gt;"page"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;truncate&lt;/span&gt;&lt;span class="err"&gt;(@&lt;/span&gt;&lt;span class="na"&gt;product.title&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;length:&lt;/span&gt; &lt;span class="err"&gt;60)&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-start justify-between gap-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h4 flex-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;product.title&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt; &lt;span class="na"&gt;current_user_stack&lt;/span&gt;&lt;span class="err"&gt;?(@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Edit&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;edit_stack_product_path&lt;/span&gt;&lt;span class="err"&gt;(@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="err"&gt;),&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;btn&lt;/span&gt; &lt;span class="na"&gt;btn-light&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;product&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the &lt;code&gt;_product.html.erb&lt;/code&gt; partial&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="c"&gt;&amp;lt;!-- app/views/products/_product.html.erb--&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;%= dom_id product %&amp;gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-start justify-between py-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt; &lt;span class="na"&gt;product.thumbnail.attached&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;image_tag&lt;/span&gt; &lt;span class="na"&gt;product.thumbnail&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col items-end"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-3xl font-bold pb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;number_to_currency&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;product.price&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Buy&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt; &lt;span class="na"&gt;Amazon&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;product.link&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target:&lt;/span&gt; &lt;span class="na"&gt;:_blank&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;btn&lt;/span&gt; &lt;span class="na"&gt;btn-dark&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"prose prose-indigo pt-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-0 font-semibold"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Description
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-0 prose"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;simple_format&lt;/span&gt; &lt;span class="na"&gt;product.description&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-0 font-semibold"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Brand
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;product.brand.name&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-0 font-semibold"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      ASIN
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;product.asin&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;time&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-600 dark:text-slate-400 text-xs mt-2"&lt;/span&gt; &lt;span class="na"&gt;datetime=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;%= product.created_at.to_formatted_s(:long) %&amp;gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Created &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;time_ago_in_words&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;product.created_at&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="na"&gt;ago&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/time&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Update the product import job to include stream trigger
&lt;/h3&gt;

&lt;p&gt;This approach aims to make the product append to the Stack product list automatically following the import (when the background job is completed). This solves the page refresh we had to do manually before. Instead of appending a new partial, we’ll re-render the whole list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductImportJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stack_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;imported_product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
    &lt;span class="c1"&gt;# check if the product already exists based on the link&lt;/span&gt;
    &lt;span class="n"&gt;existing_product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;link: &lt;/span&gt;&lt;span class="n"&gt;link&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;existing_product&lt;/span&gt;
      &lt;span class="n"&gt;imported_product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;existing_product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stacks&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="c1"&gt;# If the product doesn't exist, create a new one&lt;/span&gt;
      &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&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;link: &lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stacks&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;
      &lt;span class="n"&gt;imported_product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ProductParser&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Amazon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_to_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;broadcast_turbo_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;imported_product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;broadcast_turbo_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Turbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;StreamsChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_replace_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"product_list_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"stacks/product_list"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;stack: &lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;Turbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;StreamsChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_replace_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"products_count_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"stacks/stack_product_count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;stack: &lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new &lt;code&gt;broadcast_turbo_steam&lt;/code&gt; method gets a &lt;code&gt;stack&lt;/code&gt; and &lt;code&gt;product&lt;/code&gt; instance. We then call the &lt;code&gt;Turbo::StreamsChannel.broadcast_replace_to&lt;/code&gt; method passing the object we are streaming updates to (remember the line &lt;code&gt;&amp;lt;% turbo_stream_from @stack %&amp;gt;&lt;/code&gt; ? Yeah, that one). We'll target our product list by it's unique ID and pass the partial we made through so the whole list updates when a new product is added.&lt;/p&gt;

&lt;p&gt;Quite the feat of work to reduce the need to refresh the page, but the results are fantastic!&lt;/p&gt;

&lt;p&gt;Here’s a quick video of the new experience:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://share.cleanshot.com/vTGtCYgW"&gt;View the demo&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Add a share link to stacks
&lt;/h2&gt;

&lt;p&gt;We have most of the functionality we need to call this an MVP. We’ll need a way for users to share their stacks easily, so let's add that. When we generated a stack, we added a column called &lt;code&gt;share_link.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When a new stack is created, we could prefill that link with a unique ID that’s a short URL. Let’s add that logic to the model as a callback function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:product_stacks&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;through: :product_stacks&lt;/span&gt;

  &lt;span class="n"&gt;before_create&lt;/span&gt; &lt;span class="ss"&gt;:generate_unique_share_link&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_unique_share_link&lt;/span&gt;
    &lt;span class="c1"&gt;# Loop until a unique share link is generated&lt;/span&gt;
    &lt;span class="kp"&gt;loop&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;share_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate_random_string&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;share_link: &lt;/span&gt;&lt;span class="n"&gt;share_link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_random_string&lt;/span&gt;
    &lt;span class="no"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_base64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a new &lt;code&gt;Stack&lt;/code&gt; is created, we'll add a &lt;code&gt;share_link&lt;/code&gt; string randomized by default. We'll check to ensure it doesn't already exist so we know it's unique. Using the loop block, we can provide this.&lt;/p&gt;

&lt;p&gt;Create a new &lt;code&gt;Stack&lt;/code&gt; if you're following along and fire up your rails console.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails console

irb&amp;gt; Stack.last
&lt;span class="c"&gt;#&amp;lt;Stack:0x000000010a89cd20&lt;/span&gt;
 &lt;span class="nb"&gt;id&lt;/span&gt;: 2,
 title: &lt;span class="s2"&gt;"John's Stack"&lt;/span&gt;,
 share_link: &lt;span class="s2"&gt;"_KStndzM"&lt;/span&gt;,
 user_id: 1,
 created_at: Thu, 30 Nov 2023 22:50:00.456748000 UTC +00:00,
 updated_at: Thu, 30 Nov 2023 22:50:00.456748000 UTC +00:00&amp;gt;
irb&lt;span class="o"&gt;(&lt;/span&gt;main&lt;span class="o"&gt;)&lt;/span&gt;:003&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the &lt;code&gt;share_link&lt;/code&gt; column has a short link identifier we can now leverage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update Stack routing
&lt;/h3&gt;

&lt;p&gt;We need a way to accent the new share link as a param in our routing, so lets update the stack resources:&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="c1"&gt;# config/routes.rb&lt;/span&gt;

&lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:stacks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;param: :share_link&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:products&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'remove_thumbnail'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;on: :member&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;Here’s the full controller:&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="c1"&gt;# app/controllers/stacks_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StacksController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:authenticate_user!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;except: &lt;/span&gt;&lt;span class="sx"&gt;%i[index show]&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:set_stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="sx"&gt;%i[show edit update destroy]&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@stacks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="vi"&gt;@stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;edit&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="vi"&gt;@stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&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="n"&gt;stack_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@stack&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;redirect_to&lt;/span&gt; &lt;span class="n"&gt;stack_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Stack was successfully created."&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&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;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;stack_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Stack was successfully updated."&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&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;def&lt;/span&gt; &lt;span class="nf"&gt;destroy&lt;/span&gt;
    &lt;span class="vi"&gt;@stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy!&lt;/span&gt;
    &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;stacks_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Stack was successfully destroyed."&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_stack&lt;/span&gt;
      &lt;span class="vi"&gt;@stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;share_link: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:share_link&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&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;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:share_link&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stack_params&lt;/span&gt;
      &lt;span class="n"&gt;params&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="ss"&gt;:stack&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the &lt;code&gt;private&lt;/code&gt; method at the bottom of the file, the &lt;code&gt;set_stack&lt;/code&gt; now looks by &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;share_link&lt;/code&gt; and pulls up the same page.&lt;/p&gt;

&lt;p&gt;We can try this manually. Pull up the string your stack generated for the &lt;code&gt;share_link&lt;/code&gt; column and add it to the url &lt;code&gt;localhost:3000/stacks/_KStndzM&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It’s working great!&lt;/p&gt;

&lt;h3&gt;
  
  
  Make it easy to copy a link
&lt;/h3&gt;

&lt;p&gt;To wrap up this Let’s Build, use Stimulus.js and Clipboard.js to click an icon to copy the share link for easy access.&lt;/p&gt;

&lt;h4&gt;
  
  
  Create a stimulus controller
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g stimulus clipboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add clipboard tippy.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import the file to your &lt;code&gt;index_controller.js&lt;/code&gt; file if Rails doesn't already.&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="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;controllers&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;index_controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ClipboardController&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./clipboard_controller&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clipboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ClipboardController&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the new &lt;code&gt;clipboard_controller.js&lt;/code&gt; file, I added the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ClipboardJS&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clipboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;tippy&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tippy.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;successMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ClipboardJS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tooltip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;successMessageValue&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tooltip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorMessageValue&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;tooltip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;tippy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;showOnCreate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;onHidden&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When mounted, This reusable component will display a tooltip with a success or error message if the clipboard instance was copied. We’ll need to supply a &lt;code&gt;url&lt;/code&gt; to &lt;code&gt;clipboard.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I’ll update the &lt;code&gt;Stack&lt;/code&gt; show view to have a new button that, when clicked, should copy the share link to the user's clipboard.&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="c"&gt;&amp;lt;!-- app/views/stacks/show.html.erb--&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl mx-auto px-4 my-16"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pb-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;nav&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"breadcrumb"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-6 font-medium flex text-slate-500 dark:text-slate-200 text-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ol&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-wrap items-center space-x-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Stacks&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;stacks_path&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;hover:underline&lt;/span&gt; &lt;span class="na"&gt;hover:text-slate-600&lt;/span&gt; &lt;span class="na"&gt;dark:hover:text-slate-400&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex space-x-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-300 dark:text-slate-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;/&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-indigo-600 dark:text-indigo-500"&lt;/span&gt; &lt;span class="na"&gt;aria-current=&lt;/span&gt;&lt;span class="s"&gt;"page"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack.title&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h3 mb-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack.title&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &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;"flex items-center gap-4 py-3 px-2 border-y"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 flex items-center gap-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-white rounded-full flex items-center justify-center w-10 h-10 m-0"&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"clipboard"&lt;/span&gt; &lt;span class="na"&gt;data-clipboard-text=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;%= stack_url(@stack.share_link) %&amp;gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;link&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;classes:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;w-5&lt;/span&gt; &lt;span class="na"&gt;h-5&lt;/span&gt; &lt;span class="na"&gt;flex-shrink-0&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;time&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-slate-600 dark:text-slate-400 text-xs"&lt;/span&gt; &lt;span class="na"&gt;datetime=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;%= @stack.created_at.to_formatted_s(:long) %&amp;gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Created &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;time_ago_in_words&lt;/span&gt;&lt;span class="err"&gt;(@&lt;/span&gt;&lt;span class="na"&gt;stack.created_at&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="na"&gt;ago&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/time&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt; &lt;span class="na"&gt;current_user_stack&lt;/span&gt;&lt;span class="err"&gt;?(@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Edit&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;edit_stack_path&lt;/span&gt;&lt;span class="err"&gt;(@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;),&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;btn&lt;/span&gt; &lt;span class="na"&gt;btn-light&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;else&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;link_to&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Create&lt;/span&gt; &lt;span class="na"&gt;your&lt;/span&gt; &lt;span class="na"&gt;own&lt;/span&gt; &lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;new_user_registration_path&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;btn&lt;/span&gt; &lt;span class="na"&gt;btn-primary&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"p-6 border bg-indigo-50/20 border-indigo-600/50 rounded-2xl shadow-sm mb-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-semibold text-xl tracking-tight mb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add a product&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;form&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;product:&lt;/span&gt; &lt;span class="na"&gt;Product.new&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;products_count_#&lt;/span&gt;&lt;span class="err"&gt;{@&lt;/span&gt;&lt;span class="na"&gt;stack.id&lt;/span&gt;&lt;span class="err"&gt;}"&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;stack_product_count&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;stack:&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;product_list_#&lt;/span&gt;&lt;span class="err"&gt;{@&lt;/span&gt;&lt;span class="na"&gt;stack.id&lt;/span&gt;&lt;span class="err"&gt;}"&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;product_list&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;stack:&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;stack&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we can click to copy the URL and visit either the original stack path or the new unique share link. Awesome!&lt;/p&gt;

&lt;h3&gt;
  
  
  A gotcha
&lt;/h3&gt;

&lt;p&gt;After a quick test, I found a bug in our &lt;code&gt;products_controller.rb&lt;/code&gt;. We’re setting the Stack at the bottom in a private method, much like in the &lt;code&gt;stacks_controller.rb&lt;/code&gt; . We can copy our updated code to find the Stack like so. Important note: it's updated to &lt;code&gt;stack_share_link&lt;/code&gt; in this controller.&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="c1"&gt;# app/controllers/products_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="c1"&gt;#...&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_stack&lt;/span&gt;
       &lt;span class="vi"&gt;@stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;share_link: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:stack_share_link&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&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;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:stack_share_link&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;In conclusion, this Let's Build series focused on creating a product sharing system that allows users to add products to their stack.&lt;/p&gt;

&lt;p&gt;The system utilizes web scraping with Nokogiri to extract data from product pages on Amazon. The product's attributes, such as title, description, price, and brand, are dynamically appended to the system. The authentication strategy ensures that only the user who created the product has permission to manipulate its data.&lt;/p&gt;

&lt;p&gt;The system also allows users to edit and remove products, as well as remove product thumbnails.&lt;/p&gt;

&lt;p&gt;Creating a product dynamically involves a background job that queues up the scraping process to avoid taxing the app's resources.&lt;/p&gt;

&lt;p&gt;The system uses Nokogiri to parse the data from the product page and save it to the database. Tests are implemented to ensure the parser is returning accurate data. The system also includes the functionality to refresh the stack list of products automatically using Turbo streams. Finally, a share link is added to stacks, allowing users to share their stacks with others easily. 🎉&lt;/p&gt;

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

&lt;p&gt;I follow a methodology from the Amazon team where you start with a press release for the idea you've decided to build. If that press release is sound, it gives your team more conviction to press forward on a solid foundation. Through grit and perseverance, you can accomplish anything. This guide started with the following press release.&lt;/p&gt;

&lt;h4&gt;
  
  
  Press release
&lt;/h4&gt;

&lt;p&gt;Today, we're excited to announce the upcoming launch of Supstacker, the ultimate app for supplement enthusiasts! Designed and developed by Andy, a seasoned product designer and developer passionate about making life better through technology.&lt;/p&gt;

&lt;p&gt;Supstacker is your go-to destination for sharing and shopping for supplements. Whether you're a fitness enthusiast, health-conscious individual, or just someone looking to boost their well-being, Supstacker has got you covered.&lt;/p&gt;

&lt;p&gt;Key Features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create and Share Stacks: With Supstacker, you can easily create lists of your favorite supplements, known as "stacks." Share your stacks with friends, family, or the entire Supstacker community.&lt;/li&gt;
&lt;li&gt;Shop Smart: Find the best deals and top-quality supplements in one place. Supstacker provides product recommendations and pricing information, ensuring you get the most bang for your buck.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay tuned for the official launch of Supstacker. We can't wait to help you supercharge your supplement shopping experience. Get ready to stack, shop, and earn with Supstacker!&lt;/p&gt;

&lt;p&gt;Please get in touch with Andy Leverenz for press inquiries or more information.&lt;/p&gt;

&lt;h3&gt;
  
  
  About Supstacker
&lt;/h3&gt;

&lt;p&gt;Supstacker is Andy's brainchild, designed to simplify supplement shopping and empower users to see their friends' stacks, all while enhancing their well-being.&lt;/p&gt;

&lt;p&gt;With Supstacker, you can create, share, and shop for supplement stacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Useful links:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nokogiri.org/"&gt;Nokogiri&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stimulus.hotwired.dev/"&gt;Stimulus Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turbo.hotwired.dev/"&gt;Turbo Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/playlist?list=PL01nNIgQ4uxOhHYZd6THGIFVBALJZCcpM"&gt;Full YouTube playlist of this series&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hellorails.io"&gt;Hello Rails&lt;/a&gt; - A modern course designed to help you start using and understanding Ruby on Rails fast.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://railsui.com"&gt;Rails UI&lt;/a&gt; - Professionally designed UI components &amp;amp; templates made exclusively for Ruby on Rails.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://railsui.com/custom"&gt;Hire me&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://web-crunch.com/lets-builds"&gt;More Let's Builds like this one&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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