<?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: Zil Norvilis</title>
    <description>The latest articles on Forem by Zil Norvilis (@zilton7).</description>
    <link>https://forem.com/zilton7</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%2F352433%2F2aff65b6-dba1-4f8c-8aaf-0bc84763ae23.jpg</url>
      <title>Forem: Zil Norvilis</title>
      <link>https://forem.com/zilton7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/zilton7"/>
    <language>en</language>
    <item>
      <title>Build Your Own Cloud Empire: Hosting Unlimited Rails Apps with Kamal 2</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Tue, 05 May 2026 23:23:47 +0000</pubDate>
      <link>https://forem.com/zilton7/build-your-own-cloud-empire-hosting-unlimited-rails-apps-with-kamal-2-1bg6</link>
      <guid>https://forem.com/zilton7/build-your-own-cloud-empire-hosting-unlimited-rails-apps-with-kamal-2-1bg6</guid>
      <description>&lt;p&gt;If you are a solo developer or an indie hacker, you probably suffer from "Shiny Object Syndrome." You have 10 different ideas, and you want to launch all of them. &lt;/p&gt;

&lt;p&gt;In the old days, launching an MVP was expensive. You used Heroku or Render. You paid $7 for the web server, $9 for the database, and $5 for Redis. That is $21 per app. If you have 5 side projects, you are paying over $100 a month just to host apps that might have zero active users.&lt;/p&gt;

&lt;p&gt;This "PaaS Tax" kills innovation. It makes you afraid to launch new things.&lt;/p&gt;

&lt;p&gt;But in 2026, with the release of &lt;strong&gt;Kamal 2&lt;/strong&gt;, you don't need a Platform-as-a-Service anymore. You can rent raw, cheap Linux servers and build your own "Cloud Empire." You can host 10 different Rails apps on the exact same server, completely isolated from each other, for a fixed price of $10 a month.&lt;/p&gt;

&lt;p&gt;Here is the step-by-step guide to building your own multi-app VPS cluster using Kamal 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Strategy: The Kamal Proxy
&lt;/h2&gt;

&lt;p&gt;Before we start, you need to understand how this is possible. &lt;/p&gt;

&lt;p&gt;Normally, only one application can listen to Port 80 (HTTP) and Port 443 (HTTPS) on a server. If App A is using it, App B will crash. &lt;/p&gt;

&lt;p&gt;Kamal 2 solves this with &lt;strong&gt;Kamal Proxy&lt;/strong&gt;. When you deploy your first app, Kamal silently installs a master proxy at the front door of your server. This proxy listens to the internet. When a request comes in, the proxy looks at the domain name (&lt;code&gt;app1.com&lt;/code&gt; vs &lt;code&gt;app2.com&lt;/code&gt;) and routes the traffic to the correct Docker container. &lt;/p&gt;

&lt;p&gt;It handles the SSL certificates (HTTPS) automatically. It handles zero-downtime deployments. And it allows infinite apps to share one server.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: The Base Hardware
&lt;/h2&gt;

&lt;p&gt;First off, go to a cloud provider like Hetzner, DigitalOcean, or Linode. &lt;br&gt;
Rent a cheap Ubuntu VPS. A $10/month server on Hetzner gives you an ARM processor with 4 vCPUs and 8GB of RAM. &lt;/p&gt;

&lt;p&gt;Because Rails 8 is so efficient (especially if you use SQLite for your MVPs), 8GB of RAM is enough to comfortably run 10 to 15 separate Rails applications.&lt;/p&gt;

&lt;p&gt;Write down the IP address of your new server.&lt;/p&gt;
&lt;h2&gt;
  
  
  STEP 2: Deploying App Number 1
&lt;/h2&gt;

&lt;p&gt;Let's deploy your first idea: &lt;code&gt;project-alpha.com&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;In your first Rails application, open &lt;code&gt;config/deploy.yml&lt;/code&gt;. You configure it to point to your new server IP, and you tell Kamal Proxy which domain belongs to this app.&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;# project_alpha/config/deploy.yml&lt;/span&gt;
&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project-alpha&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your_docker_username/project-alpha&lt;/span&gt;

&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;192.168.1.100&lt;/span&gt; &lt;span class="c1"&gt;# Your Server IP&lt;/span&gt;

&lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project-alpha.com&lt;/span&gt;

&lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ... your docker registry credentials&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your terminal, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kamal setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kamal will SSH into your server, install Docker, install Kamal Proxy, issue an SSL certificate, and start &lt;code&gt;project-alpha&lt;/code&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: Deploying App Number 2 (Sharing the Server)
&lt;/h2&gt;

&lt;p&gt;Now you have a second idea: &lt;code&gt;project-beta.com&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;You do &lt;strong&gt;not&lt;/strong&gt; need to buy a second server. You just point your DNS records for &lt;code&gt;project-beta.com&lt;/code&gt; to the exact same IP address (&lt;code&gt;192.168.1.100&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;deploy.yml&lt;/code&gt; for your second Rails app:&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;# project_beta/config/deploy.yml&lt;/span&gt;
&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project-beta&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your_docker_username/project-beta&lt;/span&gt;

&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;192.168.1.100&lt;/span&gt; &lt;span class="c1"&gt;# The EXACT SAME IP&lt;/span&gt;

&lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project-beta.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;kamal setup&lt;/code&gt; in this second app. &lt;/p&gt;

&lt;p&gt;Kamal is smart enough to realize that Docker and Kamal Proxy are already installed on that server. It skips the heavy setup, deploys the new &lt;code&gt;project-beta&lt;/code&gt; container alongside the first one, and registers the new domain name with the proxy. &lt;/p&gt;

&lt;p&gt;Boom. Two apps, one server, one $10 bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 4: Scaling the Empire (The Database Node)
&lt;/h2&gt;

&lt;p&gt;Hosting 10 apps with SQLite on one server is great for MVPs. But what happens when &lt;code&gt;project-alpha&lt;/code&gt; goes viral and gets thousands of users? It starts eating all the CPU, slowing down your other 9 apps.&lt;/p&gt;

&lt;p&gt;It is time to turn your single server into a &lt;strong&gt;Cluster&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;You rent a second $10 VPS. This will be your &lt;strong&gt;Database Server&lt;/strong&gt;. &lt;br&gt;
Instead of spinning up managed RDS databases on AWS for $50/month, you use Kamal's "Accessories" feature to install a massive PostgreSQL container on this new server.&lt;/p&gt;

&lt;p&gt;In your &lt;code&gt;project-alpha&lt;/code&gt; deploy file, you separate your architecture:&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;# project_alpha/config/deploy.yml&lt;/span&gt;
&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project-alpha&lt;/span&gt;

&lt;span class="c1"&gt;# The Web server stays on Node 1&lt;/span&gt;
&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;192.168.1.100&lt;/span&gt; 

&lt;span class="c1"&gt;# The Database moves to Node 2&lt;/span&gt;
&lt;span class="na"&gt;accessories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;192.168.1.200&lt;/span&gt; &lt;span class="c1"&gt;# Your NEW Server IP&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project_alpha_prod&lt;/span&gt;
        &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
      &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD&lt;/span&gt;
    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_data:/var/lib/postgresql/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, your web traffic hits Server 1, and your database queries route securely to Server 2. You have built a distributed cloud architecture without writing a single line of Kubernetes configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The "One Person Framework" doesn't just apply to writing code. It applies to infrastructure. &lt;/p&gt;

&lt;p&gt;With Kamal 2, you have total control.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You can launch unlimited MVPs on a single cheap server.&lt;/li&gt;
&lt;li&gt;The proxy handles routing and SSL automatically.&lt;/li&gt;
&lt;li&gt;When an app gets traction, you just add another server IP to your &lt;code&gt;deploy.yml&lt;/code&gt; and scale horizontally.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You are no longer renting a tiny slice of a PaaS. You own the hardware. Go build your empire.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>devops</category>
      <category>kamal</category>
      <category>startup</category>
    </item>
    <item>
      <title>Images, Volumes, and Containers: Docker Explained in Plain English</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Mon, 04 May 2026 23:23:45 +0000</pubDate>
      <link>https://forem.com/zilton7/images-volumes-and-containers-docker-explained-in-plain-english-4l6p</link>
      <guid>https://forem.com/zilton7/images-volumes-and-containers-docker-explained-in-plain-english-4l6p</guid>
      <description>&lt;p&gt;Very often I see developers completely give up on learning Docker because of the vocabulary. &lt;/p&gt;

&lt;p&gt;You read a tutorial and the author says: &lt;em&gt;"Just build the Dockerfile into an Image, mount the Volume, map the Ports, and spin up the Container."&lt;/em&gt; If you don't have a DevOps background, that sounds like alien gibberish.&lt;/p&gt;

&lt;p&gt;The truth is, Docker is incredibly simple. It is just a virtual "shipping box" for your code. But because the creators of Docker invented their own terminology, it feels intimidating.&lt;/p&gt;

&lt;p&gt;If you want to deploy modern web apps (especially using tools like Kamal 2), you need to know what these words actually mean. Here is the absolute simplest translation of Docker jargon into plain English.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Dockerfile (The Recipe)
&lt;/h2&gt;

&lt;p&gt;Imagine you are opening a bakery. The &lt;code&gt;Dockerfile&lt;/code&gt; is your master recipe book.&lt;/p&gt;

&lt;p&gt;It is literally just a plain text file. It contains a list of instructions on exactly how to build your application's environment from a completely blank slate. &lt;/p&gt;

&lt;p&gt;It says:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Buy an oven (Install Linux).&lt;/li&gt;
&lt;li&gt;Buy some flour (Install Ruby/Node.js).&lt;/li&gt;
&lt;li&gt;Bring in the secret ingredients (Copy your app's code).&lt;/li&gt;
&lt;li&gt;Mix it all together (Run &lt;code&gt;bundle install&lt;/code&gt; or &lt;code&gt;npm install&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A &lt;code&gt;Dockerfile&lt;/code&gt; doesn't &lt;em&gt;do&lt;/em&gt; anything on its own. It is just a piece of paper waiting to be read.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Image (The Frozen Cake)
&lt;/h2&gt;

&lt;p&gt;When you run the command &lt;code&gt;docker build&lt;/code&gt;, Docker reads your recipe (the &lt;code&gt;Dockerfile&lt;/code&gt;) and goes to work. &lt;/p&gt;

&lt;p&gt;When it finishes all the steps, it takes a massive, frozen snapshot of the entire system. This snapshot is called an &lt;strong&gt;Image&lt;/strong&gt;. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  An Image is &lt;strong&gt;read-only&lt;/strong&gt;. You cannot change the code inside an Image once it is built.&lt;/li&gt;
&lt;li&gt;  It is heavy. It might be 500MB or 1GB because it contains a whole mini-operating system.&lt;/li&gt;
&lt;li&gt;  Think of the Image as the CD-ROM of a video game. The CD holds all the game files, but the CD itself doesn't "play" the game until you put it in a console.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. The Container (The Running Machine)
&lt;/h2&gt;

&lt;p&gt;This is the word people get confused by the most. &lt;/p&gt;

&lt;p&gt;If the Image is the CD-ROM, the &lt;strong&gt;Container&lt;/strong&gt; is the game actually running on your TV. &lt;/p&gt;

&lt;p&gt;When you run the command &lt;code&gt;docker run&lt;/code&gt;, Docker takes your frozen Image, wakes it up, gives it some RAM and CPU power, and turns it into a living, breathing &lt;strong&gt;Container&lt;/strong&gt;. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You can run &lt;strong&gt;10 Containers&lt;/strong&gt; from &lt;strong&gt;1 Image&lt;/strong&gt;. (Just like you can install the same CD-ROM onto 10 different computers).&lt;/li&gt;
&lt;li&gt;  Containers are completely isolated from your actual laptop. They think they are the only thing existing in the universe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Volumes (The USB Flash Drive)
&lt;/h2&gt;

&lt;p&gt;Here is the most dangerous thing about Containers: &lt;strong&gt;They have amnesia.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your Container is running your database (like PostgreSQL or SQLite), and you restart the Container or it crashes, &lt;strong&gt;all your data is permanently deleted.&lt;/strong&gt; A Container always wakes up looking exactly like the original, frozen Image.&lt;/p&gt;

&lt;p&gt;To solve this, we use &lt;strong&gt;Volumes&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;A Volume is like a USB Flash Drive that you plug into the side of the Container. You tell Docker: &lt;em&gt;"Hey, whenever the database saves a file, don't save it inside the Container's memory. Save it onto this USB drive."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This way, if the Container explodes and dies, your data is safe on the Volume. When you boot up a brand new Container tomorrow, you just plug that same Volume back in, and your database is exactly where you left off.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Ports (The Doors)
&lt;/h2&gt;

&lt;p&gt;As I mentioned earlier, Containers are isolated. They are like locked bank vaults.&lt;/p&gt;

&lt;p&gt;If you have a Rails or Node app running on port 3000 &lt;em&gt;inside&lt;/em&gt; the Container, and you open your browser to &lt;code&gt;http://localhost:3000&lt;/code&gt; on your Mac, it won't work. Your Mac cannot see inside the vault.&lt;/p&gt;

&lt;p&gt;You have to punch a hole in the wall. This is called &lt;strong&gt;Port Mapping&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;When you start the container, you pass a flag like &lt;code&gt;-p 8080:3000&lt;/code&gt;. This tells Docker:&lt;br&gt;
&lt;em&gt;"If anyone knocks on door 8080 of my actual laptop, open a secret tunnel and send them to door 3000 inside the Container."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The Registry (The App Store)
&lt;/h2&gt;

&lt;p&gt;Finally, how do you get your Image from your laptop to your production server? You use a &lt;strong&gt;Registry&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A Registry is just Dropbox for Docker Images. The most famous one is Docker Hub, but GitHub has one too (GHCR). &lt;/p&gt;

&lt;p&gt;The workflow is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You build the Image on your laptop.&lt;/li&gt;
&lt;li&gt;You &lt;code&gt;docker push&lt;/code&gt; the Image up to the Registry.&lt;/li&gt;
&lt;li&gt;You log into your cheap $5 cloud server.&lt;/li&gt;
&lt;li&gt;You &lt;code&gt;docker pull&lt;/code&gt; the Image down from the Registry.&lt;/li&gt;
&lt;li&gt;You run it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;(Side note: This exact 5-step process is what deployment tools like **Kamal&lt;/em&gt;* do automatically for you!)*&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Don't let the DevOps gatekeepers confuse you. The mental model is actually very logical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Dockerfile:&lt;/strong&gt; The blueprint.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Image:&lt;/strong&gt; The frozen snapshot built from the blueprint.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Container:&lt;/strong&gt; The running instance of the snapshot.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Volume:&lt;/strong&gt; The external hard drive to save your data.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Ports:&lt;/strong&gt; The tunnels to let internet traffic inside.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Registry:&lt;/strong&gt; The cloud storage to hold your snapshots.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once these terms click in your brain, Docker stops being a scary black box and becomes the most reliable tool in your entire development stack.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to Build a Desktop App with Rails 8 and Electron</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Sat, 02 May 2026 23:23:47 +0000</pubDate>
      <link>https://forem.com/zilton7/how-to-build-a-desktop-app-with-rails-8-and-electron-2bl8</link>
      <guid>https://forem.com/zilton7/how-to-build-a-desktop-app-with-rails-8-and-electron-2bl8</guid>
      <description>&lt;h1&gt;
  
  
  From Web to Desktop: Wrapping a Rails Hotwire App in Electron
&lt;/h1&gt;

&lt;p&gt;Very often I find myself building a successful web application, and inevitably, a user asks the golden question: &lt;em&gt;"Do you have a desktop app for Mac or Windows?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As a solo developer, hearing this used to give me anxiety. I am a Ruby developer. I don't have the time to learn Swift for macOS, C# for Windows, or rewrite my entire frontend in React just to use a desktop framework.&lt;/p&gt;

&lt;p&gt;But in 2026, you don't have to. You can take your existing Rails app, wrap it in &lt;strong&gt;Electron&lt;/strong&gt;, and ship it as a native desktop application. &lt;/p&gt;

&lt;p&gt;The secret reason this works so well today is &lt;strong&gt;Hotwire&lt;/strong&gt;. Because Turbo intercepts link clicks and updates the page without doing a full browser reload, your web app inside Electron doesn't have that ugly "white flash" between pages. It actually feels like a native desktop app.&lt;/p&gt;

&lt;p&gt;Here is how to wrap your Rails app into an Electron desktop app in 4 easy steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: The Electron Setup
&lt;/h2&gt;

&lt;p&gt;Since Electron runs on Node.js, we do need to step out of the Ruby world just for a minute to create our desktop "shell". &lt;/p&gt;

&lt;p&gt;Create a new folder completely separate from your Rails app, and initialize a basic Node project:&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;mkdir &lt;/span&gt;my-desktop-app
&lt;span class="nb"&gt;cd &lt;/span&gt;my-desktop-app
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;electron &lt;span class="nt"&gt;--save-dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your &lt;code&gt;package.json&lt;/code&gt;, update the main entry point and add a start script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-desktop-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main.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;"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;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"electron ."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 2: The Main Process (Loading Rails)
&lt;/h2&gt;

&lt;p&gt;Now we create the &lt;code&gt;main.js&lt;/code&gt; file. This is the brain of your desktop app. Its entire job is to boot up a chromium window and point it to your Rails server.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;main.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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shell&lt;/span&gt; &lt;span class="p"&gt;}&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="s1"&gt;electron&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="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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createWindow&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;win&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;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;titleBarStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hiddenInset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Makes it look modern on Mac&lt;/span&gt;
    &lt;span class="na"&gt;webPreferences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;preload&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;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preload.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;nodeIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;contextIsolation&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="c1"&gt;// In development, point to localhost. &lt;/span&gt;
  &lt;span class="c1"&gt;// In production, point to your real HTTPS domain.&lt;/span&gt;
  &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// STEP 3 LOGIC GOES HERE...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whenReady&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;createWindow&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;If you run &lt;code&gt;npm start&lt;/code&gt; right now (while your Rails server is running on port 3000), a beautiful desktop window will open, displaying your Rails app!&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: Handling External Links
&lt;/h2&gt;

&lt;p&gt;There is one immediate problem. If a user clicks a link in your app that goes to &lt;code&gt;https://twitter.com&lt;/code&gt;, Twitter will load &lt;em&gt;inside&lt;/em&gt; your Electron app. You don't want that. You want external links to open in the user's default browser (like Chrome or Safari).&lt;/p&gt;

&lt;p&gt;We can intercept these links inside &lt;code&gt;main.js&lt;/code&gt;. Add this inside your &lt;code&gt;createWindow&lt;/code&gt; function:&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;// Intercept links opening in new windows&lt;/span&gt;
  &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webContents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setWindowOpenHandler&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// If the URL is not your Rails app, open it in the default browser&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;url&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;localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openExternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deny&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;allow&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;&lt;em&gt;Note: In Rails, make sure your external links have &lt;code&gt;target="_blank"&lt;/code&gt; so Electron knows it is trying to open a new window!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 4: The Hotwire Bridge (Stimulus)
&lt;/h2&gt;

&lt;p&gt;Your app looks great, but what if you want to use Native Desktop features? What if you want to trigger a native Desktop Notification when a Rails background job finishes?&lt;/p&gt;

&lt;p&gt;We need our Rails app to "talk" to Electron. We do this using a &lt;code&gt;preload.js&lt;/code&gt; file. &lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;preload.js&lt;/code&gt; in your Electron folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contextBridge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt; &lt;span class="p"&gt;}&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="s1"&gt;electron&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// We expose a safe API to the browser window&lt;/span&gt;
&lt;span class="nx"&gt;contextBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exposeInMainWorld&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desktopAPI&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;sendNotification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&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;// We send a message to the Electron main process&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&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;Now, back in your &lt;strong&gt;Rails App&lt;/strong&gt;, you can write a simple Stimulus controller to trigger this!&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 desktop
&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;// app/javascript/controllers/desktop_controller.js&lt;/span&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="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// We check if 'window.desktopAPI' exists. &lt;/span&gt;
    &lt;span class="c1"&gt;// If they are using a normal web browser, it will be undefined.&lt;/span&gt;
    &lt;span class="c1"&gt;// If they are in our Electron app, it will run!&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;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;desktopAPI&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;desktopAPI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Task Complete&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;Your export is ready.&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="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Task Complete (Web Version)&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;Attach this controller to a button in your Rails view: &lt;code&gt;&amp;lt;button data-controller="desktop" data-action="click-&amp;gt;desktop#notify"&amp;gt;Test Notification&amp;lt;/button&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The "Write Once, Run Anywhere" dream used to be a myth. But the modern Rails stack makes it incredibly close to reality.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Your &lt;strong&gt;Rails backend&lt;/strong&gt; handles the database and logic.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Hotwire&lt;/strong&gt; makes the frontend feel instant and native.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Hotwire Native&lt;/strong&gt; wraps it for iOS and Android.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Electron&lt;/strong&gt; wraps it for macOS and Windows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By simply pointing an Electron window at your Rails URL and exposing a few features via a Stimulus bridge, you can offer your users a premium desktop experience without maintaining a separate codebase.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>electron</category>
      <category>hotwire</category>
      <category>desktop</category>
    </item>
    <item>
      <title>High ROI Testing: The 3 Secrets Every Solo Rails Dev Needs</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Fri, 01 May 2026 23:11:26 +0000</pubDate>
      <link>https://forem.com/zilton7/high-roi-testing-the-3-secrets-every-solo-rails-dev-needs-56hk</link>
      <guid>https://forem.com/zilton7/high-roi-testing-the-3-secrets-every-solo-rails-dev-needs-56hk</guid>
      <description>&lt;p&gt;Very often I see solo developers fall into one of two traps. &lt;/p&gt;

&lt;p&gt;Trap number one: They read a book on Enterprise Test-Driven Development (TDD) and spend 3 weeks writing unit tests for a product that has zero users. They burn out and quit.&lt;/p&gt;

&lt;p&gt;Trap number two: They write zero tests because they want to "move fast." Then, their app gets its first 100 users, and they become completely paralyzed. They are terrified to push a new feature on a Friday because they don't know if they will break the checkout page.&lt;/p&gt;

&lt;p&gt;When you are a "One-Person Team," your scarcest resource is time. You cannot test everything. You need to focus on &lt;strong&gt;High ROI (Return on Investment) Testing&lt;/strong&gt;. You need the absolute maximum amount of confidence with the minimum amount of code.&lt;/p&gt;

&lt;p&gt;Here are my top 3 tips for testing as a solo developer to keep your momentum high and your anxiety low.&lt;/p&gt;

&lt;h2&gt;
  
  
  TIP 1: System Tests &amp;gt; Unit Tests
&lt;/h2&gt;

&lt;p&gt;If you only have time to write one type of test, ignore the models and ignore the controllers. Write &lt;strong&gt;System Tests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A system test boots up a real browser in the background, goes to your URL, and clicks buttons just like a human user would. &lt;/p&gt;

&lt;p&gt;Why is this so powerful? Because a single system test verifies your routing, your controller logic, your database queries, and your HTML rendering all at once. If you rewrite your entire backend logic, your system test doesn't care. It just cares that the user can still click the button.&lt;/p&gt;

&lt;p&gt;Focus on testing the "Golden Paths" - the actions that actually make you money.&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;# test/system/checkout_test.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"application_system_test_case"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckoutTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationSystemTestCase&lt;/span&gt;
  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"user can sign up and pay for a subscription"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;visit&lt;/span&gt; &lt;span class="n"&gt;pricing_path&lt;/span&gt;
    &lt;span class="n"&gt;click_on&lt;/span&gt; &lt;span class="s2"&gt;"Buy Pro"&lt;/span&gt;

    &lt;span class="n"&gt;fill_in&lt;/span&gt; &lt;span class="s2"&gt;"Email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with: &lt;/span&gt;&lt;span class="s2"&gt;"customer@example.com"&lt;/span&gt;
    &lt;span class="n"&gt;fill_in&lt;/span&gt; &lt;span class="s2"&gt;"Password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with: &lt;/span&gt;&lt;span class="s2"&gt;"secret123"&lt;/span&gt;
    &lt;span class="n"&gt;click_on&lt;/span&gt; &lt;span class="s2"&gt;"Sign Up"&lt;/span&gt;

    &lt;span class="n"&gt;assert_text&lt;/span&gt; &lt;span class="s2"&gt;"Welcome to Pro!"&lt;/span&gt;
    &lt;span class="n"&gt;assert&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;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pro_plan?&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;
  
  
  TIP 2: Embrace Rails Fixtures (Ditch FactoryBot)
&lt;/h2&gt;

&lt;p&gt;The Ruby community loves &lt;code&gt;FactoryBot&lt;/code&gt;. Factories are incredibly flexible, but they have a massive hidden cost: &lt;strong&gt;Speed&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Factories dynamically generate and insert records into your database on &lt;em&gt;every single test run&lt;/em&gt;. If you have a complex app, running 50 tests can suddenly take 30 seconds. If your tests are slow, you will stop running them.&lt;/p&gt;

&lt;p&gt;As a solo dev, you should use the Rails default: &lt;strong&gt;Fixtures&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Fixtures are simple YAML files that get loaded directly into your test database &lt;em&gt;exactly once&lt;/em&gt; when the test suite boots.&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;zil&lt;/span&gt;&lt;span class="pi"&gt;:&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;zil@example.com&lt;/span&gt;
  &lt;span class="na"&gt;encrypted_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= User.new.send(:password_digest, 'password') %&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;active&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the data is already in the database before the test even starts, a Minitest suite using fixtures runs in a fraction of a second. Fast tests mean you will actually use them while you code.&lt;/p&gt;

&lt;h2&gt;
  
  
  TIP 3: Never Hit Real APIs (Use VCR)
&lt;/h2&gt;

&lt;p&gt;If your app talks to Stripe, OpenAI, or a weather API, do not let your test suite make real network requests. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It slows your tests down to a crawl.&lt;/li&gt;
&lt;li&gt;It makes your tests flaky (if the API is down, your test fails).&lt;/li&gt;
&lt;li&gt;You might accidentally hit a rate limit and get banned.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead, use a gem called &lt;strong&gt;VCR&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;VCR intercepts the HTTP request the very first time you run the test, saves the API's response to a local text file (a "cassette"), and then replays that text file instantly on all future test runs.&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;OpenAiServiceTest&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="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"generates a summary from the API"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# VCR intercepts the network call perfectly&lt;/span&gt;
    &lt;span class="no"&gt;VCR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use_cassette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"openai_summary"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OpenAiService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Long text here..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;assert_match&lt;/span&gt; &lt;span class="sr"&gt;/Expected output/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&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;Your tests run offline, instantly, and with 100% predictable results.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Shameless Plug: Want to master this workflow?&lt;/strong&gt;&lt;br&gt;
If you are tired of guessing what to test, fighting with flaky setups, or feeling anxious every time you deploy, I’ve packaged my entire exact testing strategy into a comprehensive guide. &lt;/p&gt;

&lt;p&gt;Check out &lt;strong&gt;&lt;a href="https://norvilis.gumroad.com/l/wise-testing" rel="noopener noreferrer"&gt;Wise Testing: The Solo Developer's Guide to Rails Testing&lt;/a&gt;&lt;/strong&gt;. You will learn how to set up lightning-fast Minitest suites, test complex Webhooks, and build a safety net that protects your startup without slowing you down.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Testing doesn't have to be a chore that steals time away from building features. If you approach it pragmatically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Test the user experience (System Tests).&lt;/li&gt;
&lt;li&gt;Optimize for speed (Fixtures).&lt;/li&gt;
&lt;li&gt;Isolate the network (VCR).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You will end up with a safety net that lets you code fearlessly and deploy on Fridays. &lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>testing</category>
      <category>startup</category>
    </item>
    <item>
      <title>The Importmap Guide to Shadcn: Beautiful UI with Zero Build Step</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Thu, 30 Apr 2026 23:11:28 +0000</pubDate>
      <link>https://forem.com/zilton7/the-importmap-guide-to-shadcn-beautiful-ui-with-zero-build-step-4ane</link>
      <guid>https://forem.com/zilton7/the-importmap-guide-to-shadcn-beautiful-ui-with-zero-build-step-4ane</guid>
      <description>&lt;h1&gt;
  
  
  How to Use Shadcn UI in Rails 8 (Without React or Webpack)
&lt;/h1&gt;

&lt;p&gt;If you have looked at frontend design anytime recently, you have definitely seen &lt;strong&gt;shadcn/ui&lt;/strong&gt;. It is the most popular UI library in the world right now. It looks beautiful, minimal, and highly professional.&lt;/p&gt;

&lt;p&gt;Very often I find myself wanting to use these beautiful components in my Rails apps. But immediately, I hit a massive wall. &lt;/p&gt;

&lt;p&gt;Shadcn is built strictly for &lt;strong&gt;React&lt;/strong&gt;. Its installation instructions ask you to run &lt;code&gt;npx&lt;/code&gt;, install a bunch of Node.js dependencies, and compile JSX code. &lt;/p&gt;

&lt;p&gt;If you are using Rails 8 with &lt;strong&gt;Importmaps&lt;/strong&gt;, you do not have a build step. You do not have Node.js. You cannot just run &lt;code&gt;bin/importmap pin shadcn&lt;/code&gt; because browsers do not understand React JSX natively.&lt;/p&gt;

&lt;p&gt;Does this mean we can't use it? No. &lt;br&gt;
Shadcn is just a combination of two things: &lt;strong&gt;Tailwind CSS&lt;/strong&gt; (for the design) and &lt;strong&gt;Radix UI&lt;/strong&gt; (for the JavaScript behavior). We can easily replicate this in pure Rails using Importmaps and Stimulus. &lt;/p&gt;

&lt;p&gt;Here is exactly how to get the Shadcn experience in a modern Rails 8 app.&lt;/p&gt;
&lt;h2&gt;
  
  
  STEP 1: The Reality Check (What are we actually doing?)
&lt;/h2&gt;

&lt;p&gt;Because we are not using React, we are going to do what I call "Design Extraction." &lt;/p&gt;

&lt;p&gt;We are going to steal the beautiful HTML and Tailwind classes from Shadcn, and we are going to replace the heavy React JavaScript with lightweight &lt;strong&gt;Stimulus&lt;/strong&gt; controllers.&lt;/p&gt;

&lt;p&gt;To do this, your Rails 8 app needs to have Tailwind CSS installed. If you generated your app with Tailwind, you are ready to go. If not, you can add it easily:&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;h2&gt;
  
  
  STEP 2: Pin the Stimulus Helpers (Importmap)
&lt;/h2&gt;

&lt;p&gt;Shadcn components like Modals, Dropdowns, and Tabs need JavaScript to open and close. We don't want to write this Javascript from scratch.&lt;/p&gt;

&lt;p&gt;Instead, we will use an amazing open-source library called &lt;code&gt;tailwindcss-stimulus-components&lt;/code&gt;. It provides pre-written Stimulus controllers that do exactly what Shadcn's React code does (toggling classes, handling clicks, managing focus).&lt;/p&gt;

&lt;p&gt;Since we are using Importmaps, we install it straight from the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/importmap pin tailwindcss-stimulus-components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we need to register these controllers in our Javascript 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="c1"&gt;// app/javascript/controllers/index.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;application&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;controllers/application&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Dropdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Tabs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Popover&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;tailwindcss-stimulus-components&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="s1"&gt;dropdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Dropdown&lt;/span&gt;&lt;span class="p"&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="s1"&gt;modal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Modal&lt;/span&gt;&lt;span class="p"&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="s1"&gt;tabs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Tabs&lt;/span&gt;&lt;span class="p"&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="s1"&gt;popover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Popover&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: Stealing the Shadcn Design
&lt;/h2&gt;

&lt;p&gt;Let's build a Shadcn-style &lt;strong&gt;Dropdown Menu&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;If you go to the Shadcn documentation, you will see a beautiful React component. We ignore the React code. We just look at the design: white background, subtle border, small shadows, and rounded corners.&lt;/p&gt;

&lt;p&gt;We write plain HTML, apply those exact Tailwind classes, and wrap it in our new Stimulus &lt;code&gt;dropdown&lt;/code&gt; controller:&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="c"&gt;&amp;lt;!-- A Shadcn-style Dropdown in pure Rails --&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;"dropdown"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative inline-block text-left"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- The Trigger Button --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"click-&amp;gt;dropdown#toggle click@window-&amp;gt;dropdown#hide"&lt;/span&gt; 
          &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline-flex items-center justify-center rounded-md text-sm font-medium border border-gray-200 bg-white px-4 py-2 hover:bg-gray-100 hover:text-gray-900 transition-colors"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Open Menu
  &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- The Dropdown Content (Hidden by default) --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-dropdown-target=&lt;/span&gt;&lt;span class="s"&gt;"menu"&lt;/span&gt; 
       &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hidden absolute right-0 mt-2 w-56 rounded-md border border-gray-200 bg-white shadow-md p-1 z-50 transition transform origin-top-right"&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-2 py-1.5 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 rounded-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Profile
    &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-2 py-1.5 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 rounded-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Settings
    &lt;span class="nt"&gt;&amp;lt;/a&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;"h-px bg-gray-200 my-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt; &lt;span class="c"&gt;&amp;lt;!-- Divider --&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;"Log out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destroy_user_session_path&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;"w-full text-left px-2 py-1.5 text-sm text-red-600 hover:bg-gray-100 rounded-sm"&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;strong&gt;Why this works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The CSS gives it the exact premium, crisp look of Shadcn.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;data-controller="dropdown"&lt;/code&gt; uses the Stimulus library we pinned via Importmap to handle the open/close logic and clicking outside the menu. &lt;/li&gt;
&lt;li&gt;We didn't write a single line of custom Javascript or touch Webpack.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  STEP 4: Creating Reusable Components (The Rails Way)
&lt;/h2&gt;

&lt;p&gt;The magic of Shadcn in React is that you just write &lt;code&gt;&amp;lt;Button&amp;gt;Click me&amp;lt;/Button&amp;gt;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;If you copy-paste the massive Tailwind HTML above every time you need a dropdown, your views will become a nightmare. In Rails, we solve this by wrapping our new designs into &lt;strong&gt;Rails Helpers&lt;/strong&gt; or &lt;strong&gt;ViewComponents&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's turn the famous Shadcn Button into a simple Rails helper.&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/helpers/ui_helper.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;UiHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shadcn_button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;style: :primary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;base_classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 px-4 py-2"&lt;/span&gt;

    &lt;span class="n"&gt;style_classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="ss"&gt;:primary&lt;/span&gt;
      &lt;span class="s2"&gt;"bg-slate-900 text-white hover:bg-slate-900/90"&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="ss"&gt;:secondary&lt;/span&gt;
      &lt;span class="s2"&gt;"bg-slate-100 text-slate-900 hover:bg-slate-100/80"&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="ss"&gt;:destructive&lt;/span&gt;
      &lt;span class="s2"&gt;"bg-red-500 text-white hover:bg-red-500/90"&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="ss"&gt;:outline&lt;/span&gt;
      &lt;span class="s2"&gt;"border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;css_classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;base_classes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;style_classes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:class&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="n"&gt;css_classes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;except&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:class&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;Now, anywhere in your app, you can just write:&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;shadcn_button&lt;/span&gt; &lt;span class="s2"&gt;"Save Changes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;save_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;style: :primary&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;shadcn_button&lt;/span&gt; &lt;span class="s2"&gt;"Delete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delete_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;style: :destructive&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Don't let the React community trick you into thinking you need a massive Javascript build step just to have beautiful buttons and modals.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shadcn is just a design system.&lt;/strong&gt; It is Tailwind CSS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React is just the engine.&lt;/strong&gt; We replace it with Stimulus via Importmaps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Components are just helpers.&lt;/strong&gt; We replace JSX with Rails Helpers or ViewComponents.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By combining &lt;code&gt;tailwindcss-stimulus-components&lt;/code&gt; and some smart Rails helpers, you can get 100% of the visual beauty of Shadcn while keeping the blazing fast, no-build-step architecture of Rails 8.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>frontend</category>
      <category>tailwindcss</category>
      <category>stimulus</category>
    </item>
    <item>
      <title>The Secret to Side-by-Side Gem Development in Rails</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Wed, 29 Apr 2026 23:10:27 +0000</pubDate>
      <link>https://forem.com/zilton7/the-secret-to-side-by-side-gem-development-in-rails-4h93</link>
      <guid>https://forem.com/zilton7/the-secret-to-side-by-side-gem-development-in-rails-4h93</guid>
      <description>&lt;h1&gt;
  
  
  Stop Pushing to GitHub: How to Test Ruby Gems Locally
&lt;/h1&gt;

&lt;p&gt;Very often I find myself writing a piece of code in a Rails app and thinking: &lt;em&gt;"I use this in all my projects. I should extract this into a Ruby Gem."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Building a gem is a great way to clean up your codebase and contribute to open-source. But the first time you try to build one, the testing process feels awful. &lt;/p&gt;

&lt;p&gt;Most beginners write some code in their gem, push it to GitHub, go to their Rails app, run &lt;code&gt;bundle update my_gem&lt;/code&gt;, restart the server, and see if it worked. If there is a typo, they have to do the whole 5-minute loop all over again.&lt;/p&gt;

&lt;p&gt;You do not need to do this. You can develop your gem and your Rails app side-by-side on your local machine, and see your changes instantly. Here is exactly how to set it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: The Folder Structure
&lt;/h2&gt;

&lt;p&gt;To keep things organized, both your gem and your Rails app should live in the same parent directory on your computer.&lt;/p&gt;

&lt;p&gt;Open your terminal and create a new gem using Bundler's built-in generator:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This creates a folder called &lt;code&gt;awesome_tool&lt;/code&gt; with all the boilerplate code you need for a gem. Next, go back to the parent folder and create a dummy Rails app to test it on:&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 test_app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your folder structure looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;~/projects/awesome_tool&lt;/code&gt; (Your Gem)&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;~/projects/test_app&lt;/code&gt; (Your Rails App)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  STEP 2: The Magic Link (&lt;code&gt;path&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Now we need to tell our Rails app to use the local folder instead of downloading the gem from the internet.&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;Gemfile&lt;/code&gt; inside your &lt;code&gt;test_app&lt;/code&gt; and add this line:&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;# test_app/Gemfile&lt;/span&gt;

&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'awesome_tool'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;path: &lt;/span&gt;&lt;span class="s1"&gt;'../awesome_tool'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;path:&lt;/code&gt; option is the secret sauce. It tells Bundler to look in the neighboring folder on your hard drive. &lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;bundle install&lt;/code&gt; in your Rails app. You will see Bundler confirm that it is using the local source.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: Write Code in the Gem
&lt;/h2&gt;

&lt;p&gt;Let's add a simple feature to our gem to make sure they are connected. &lt;/p&gt;

&lt;p&gt;Open the main file inside your gem directory (&lt;code&gt;awesome_tool/lib/awesome_tool.rb&lt;/code&gt;) and write a simple 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;# awesome_tool/lib/awesome_tool.rb&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s2"&gt;"awesome_tool/version"&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;AwesomeTool&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;say_hello&lt;/span&gt;
    &lt;span class="s2"&gt;"Hello from the local gem!"&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;
  
  
  STEP 4: Test it in Rails
&lt;/h2&gt;

&lt;p&gt;Go back to your &lt;code&gt;test_app&lt;/code&gt; terminal and open the 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type &lt;code&gt;AwesomeTool.say_hello&lt;/code&gt;. It will output &lt;code&gt;"Hello from the local gem!"&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;If you go back to your gem, change the text to &lt;code&gt;"Wow, this is fast!"&lt;/code&gt;, and then restart your Rails console, the new text will be there instantly. No Git commits, no pushing to the internet. &lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 5: The Pro Move (&lt;code&gt;bundle config&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;path:&lt;/code&gt; trick is perfect when you are building a brand new gem from scratch. &lt;/p&gt;

&lt;p&gt;But what if you are contributing to a huge open-source gem (like &lt;code&gt;devise&lt;/code&gt; or &lt;code&gt;sidekiq&lt;/code&gt;)? You want to test your bug fix on your company's real Rails app. &lt;br&gt;
However, you &lt;strong&gt;cannot&lt;/strong&gt; change the &lt;code&gt;Gemfile&lt;/code&gt; to &lt;code&gt;path: '../devise'&lt;/code&gt;, because if you accidentally commit that &lt;code&gt;Gemfile&lt;/code&gt; to GitHub, you will break the app for all your coworkers!&lt;/p&gt;

&lt;p&gt;For this, we use the &lt;code&gt;bundle config local&lt;/code&gt; trick.&lt;/p&gt;

&lt;p&gt;Leave your Rails &lt;code&gt;Gemfile&lt;/code&gt; pointing to GitHub normally:&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="s1"&gt;'sidekiq'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;github: &lt;/span&gt;&lt;span class="s1"&gt;'sidekiq/sidekiq'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your terminal, tell Bundler to &lt;em&gt;override&lt;/em&gt; that specific gem locally on your computer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle config local.sidekiq /Users/yourname/projects/sidekiq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, every time you run your Rails app, it will silently use your local folder instead of GitHub. Your &lt;code&gt;Gemfile&lt;/code&gt; stays perfectly clean, so you can never accidentally commit a broken path to production. &lt;/p&gt;

&lt;p&gt;To turn this off when you are done testing, just 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 config &lt;span class="nt"&gt;--delete&lt;/span&gt; local.sidekiq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Building gems doesn't have to be a slow, frustrating process. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;gem 'name', path: '../name'&lt;/code&gt; for quick, brand new gems you are building yourself.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;bundle config local.name /path&lt;/code&gt; when testing changes to established gems without messing up your team's &lt;code&gt;Gemfile&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Extracting your complex logic into local gems is a fantastic way to clean up a messy Rails monolith, and this workflow makes it completely painless.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>gems</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Language of the Web: HTTP Basics You Actually Need to Know</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Tue, 28 Apr 2026 23:11:32 +0000</pubDate>
      <link>https://forem.com/zilton7/the-language-of-the-web-http-basics-you-actually-need-to-know-4kem</link>
      <guid>https://forem.com/zilton7/the-language-of-the-web-http-basics-you-actually-need-to-know-4kem</guid>
      <description>&lt;h1&gt;
  
  
  HTTP Basics You Must Know As A Web Developer
&lt;/h1&gt;

&lt;p&gt;Very often I see new developers build an entire web application using Rails, React, or Laravel. They know how to write database queries and build components. But the moment they try to connect to a third-party API and get a weird "401 Unauthorized" or "CORS" error, they completely freeze.&lt;/p&gt;

&lt;p&gt;The problem is that modern frameworks are almost &lt;em&gt;too&lt;/em&gt; good. They hide the raw communication of the internet behind "magic" methods. &lt;/p&gt;

&lt;p&gt;Underneath all the Ruby, PHP, and JavaScript, the internet runs on a very simple text-based language called &lt;strong&gt;HTTP&lt;/strong&gt; (Hypertext Transfer Protocol). If you don't understand how HTTP works, debugging web apps is incredibly painful.&lt;/p&gt;

&lt;p&gt;Here are the 5 core concepts of HTTP that you absolutely must know to be a confident web developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  CONCEPT 1: The Request and Response Cycle
&lt;/h2&gt;

&lt;p&gt;HTTP is just a conversation between two computers: the &lt;strong&gt;Client&lt;/strong&gt; (your browser or mobile app) and the &lt;strong&gt;Server&lt;/strong&gt; (your Rails/Node app).&lt;/p&gt;

&lt;p&gt;The conversation is strictly turn-based. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Client asks a question (The Request).&lt;/li&gt;
&lt;li&gt;The Server gives an answer (The Response).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The server will &lt;em&gt;never&lt;/em&gt; speak to the client unless the client speaks first. (This is why WebSockets were invented later, to allow two-way chatting, but standard HTTP is always a 1-way ask-and-receive).&lt;/p&gt;

&lt;p&gt;If you strip away your framework, a raw HTTP Request literally just looks like this simple text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/users&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api.myapp.com&lt;/span&gt;
&lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the Server's Response looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Zil"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CONCEPT 2: The Verbs (Methods)
&lt;/h2&gt;

&lt;p&gt;When the Client sends a request, it has to tell the Server what &lt;em&gt;action&lt;/em&gt; it wants to perform. We use "Verbs" for this. &lt;/p&gt;

&lt;p&gt;If you know database CRUD (Create, Read, Update, Delete), these map perfectly to HTTP verbs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;GET (Read):&lt;/strong&gt; "Give me some data." (e.g., loading a webpage or fetching a user profile). GET requests should &lt;em&gt;never&lt;/em&gt; change the database.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;POST (Create):&lt;/strong&gt; "Here is some new data, save it." (e.g., submitting a sign-up form).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;PUT / PATCH (Update):&lt;/strong&gt; "Here is some updated data, change an existing record." (PATCH is for partial updates, PUT usually replaces the whole record).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;DELETE (Destroy):&lt;/strong&gt; "Delete this record."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Rookie Mistake:&lt;/em&gt; Never use a GET request to delete a user (like &lt;code&gt;/users/delete/1&lt;/code&gt;). If Google's web crawler bots find that link, they will visit it and accidentally delete your users! Always use the correct verb.&lt;/p&gt;

&lt;h2&gt;
  
  
  CONCEPT 3: Status Codes (The 5 Buckets)
&lt;/h2&gt;

&lt;p&gt;When the Server responds, it gives a 3-digit number called a Status Code. You don't need to memorize all 60 of them. You just need to know the 5 main buckets.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;1xx (Informational):&lt;/strong&gt; "Hang on, I'm thinking." (You rarely see these).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;2xx (Success):&lt;/strong&gt; "Everything went perfectly!"

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;200 OK&lt;/code&gt;: Standard success.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;201 Created&lt;/code&gt;: Successfully saved a new record.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  &lt;strong&gt;3xx (Redirection):&lt;/strong&gt; "The thing you want is over there."

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;301/302&lt;/code&gt;: Redirecting the user to a new URL.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  &lt;strong&gt;4xx (Client Error):&lt;/strong&gt; "YOU messed up."

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;400 Bad Request&lt;/code&gt;: You sent missing or invalid data.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;401 Unauthorized&lt;/code&gt;: You forgot your API key or aren't logged in.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;403 Forbidden&lt;/code&gt;: You are logged in, but you don't have admin rights to see this.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;404 Not Found&lt;/code&gt;: You asked for a URL that doesn't exist.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  &lt;strong&gt;5xx (Server Error):&lt;/strong&gt; "I (the server) messed up."

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;500 Internal Server Error&lt;/code&gt;: Your backend code crashed. Check your server logs!&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  CONCEPT 4: Headers (The Hidden Metadata)
&lt;/h2&gt;

&lt;p&gt;Both the Request and the Response have "Headers". Headers are just key-value pairs of hidden information. They are the metadata of the conversation.&lt;/p&gt;

&lt;p&gt;As a developer, you will deal with two headers constantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Content-Type:&lt;/strong&gt;&lt;br&gt;
This tells the other computer what kind of data is in the body of the message. &lt;br&gt;
If your Rails API sends JSON, but forgets to set &lt;code&gt;Content-Type: application/json&lt;/code&gt;, the frontend Javascript might think it's just raw text and crash when trying to parse it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Authorization:&lt;/strong&gt;&lt;br&gt;
This is how APIs handle security. Instead of sending a username and password every time, you usually pass a token in the headers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Authorization: Bearer my_secret_api_key_123
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CONCEPT 5: Statelessness (The Goldfish Memory)
&lt;/h2&gt;

&lt;p&gt;This is the most important concept to grasp. &lt;strong&gt;HTTP is completely stateless.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;HTTP has the memory of a goldfish. If you send a request to a server, and then send a second request 1 millisecond later, the server has absolutely no idea that you are the same person. It treats every single request as a brand new stranger.&lt;/p&gt;

&lt;p&gt;So, how does a website keep you "logged in"? &lt;br&gt;
&lt;strong&gt;Cookies.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you log in successfully (POST request), the Server's response includes a special header: &lt;code&gt;Set-Cookie: session_id=abc123&lt;/code&gt;.&lt;br&gt;
Your browser saves that cookie. On your next request, your browser automatically attaches that cookie in the headers. The server reads the cookie and says, &lt;em&gt;"Ah, I remember this session ID, you are logged in."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;When an API integration breaks, don't just stare at your Ruby or Javascript code. Look at the raw HTTP request in your browser's Network tab.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Did you use the right &lt;strong&gt;Verb&lt;/strong&gt;? (POST instead of GET?)&lt;/li&gt;
&lt;li&gt;What was the &lt;strong&gt;Status Code&lt;/strong&gt;? (401 means check your auth token, 500 means check your server logs).&lt;/li&gt;
&lt;li&gt;Are your &lt;strong&gt;Headers&lt;/strong&gt; correct? (Is Content-Type set to JSON?)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you understand that HTTP is just simple text being passed back and forth, web development becomes a lot less scary and a lot more logical.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>http</category>
      <category>beginners</category>
      <category>api</category>
    </item>
    <item>
      <title>Stop Using Ugly Browser Alerts: Custom Turbo Confirms in Rails 8</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Mon, 27 Apr 2026 23:11:26 +0000</pubDate>
      <link>https://forem.com/zilton7/stop-using-ugly-browser-alerts-custom-turbo-confirms-in-rails-8-2n42</link>
      <guid>https://forem.com/zilton7/stop-using-ugly-browser-alerts-custom-turbo-confirms-in-rails-8-2n42</guid>
      <description>&lt;p&gt;I'm building a really nice, modern Rails application. I use Tailwind to make the buttons look great, I use Hotwire to make the page load instantly, and then... I click a "Delete" button.&lt;/p&gt;

&lt;p&gt;Suddenly, the screen freezes and a massive, ugly gray alert box from 1998 pops up saying &lt;em&gt;"Are you sure?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the default browser &lt;code&gt;window.confirm()&lt;/code&gt; dialog. It looks terrible, it breaks the modern feel of your app, and you cannot style it with CSS. &lt;/p&gt;

&lt;p&gt;For a long time, fixing this required writing a bunch of messy custom Javascript to intercept form submissions. But in modern Rails (using Turbo), overriding this behavior is actually incredibly simple. We can use a single Promise and the native HTML5 &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element to create a beautiful, custom confirmation modal.&lt;/p&gt;

&lt;p&gt;Here is exactly how to do it in 3 easy steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: The Native HTML Dialog
&lt;/h2&gt;

&lt;p&gt;First off, we need the actual modal that will pop up. Instead of importing heavy JavaScript modal libraries, we are going to use the native HTML5 &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element. It is supported in all modern browsers and handles all the annoying overlay math for us.&lt;/p&gt;

&lt;p&gt;Open your main application layout file and drop this code right before the closing &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; tag.&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="c"&gt;&amp;lt;!-- app/views/layouts/application.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;dialog&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"turbo-confirm-modal"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"backdrop:bg-black/50 p-6 rounded-xl shadow-xl w-96 bg-white"&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;id=&lt;/span&gt;&lt;span class="s"&gt;"turbo-confirm-message"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-xl font-bold mb-6 text-gray-800"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Are you sure?
  &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;"flex justify-end gap-3"&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;id=&lt;/span&gt;&lt;span class="s"&gt;"turbo-confirm-cancel"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Cancel
    &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;id=&lt;/span&gt;&lt;span class="s"&gt;"turbo-confirm-accept"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Confirm
    &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;/dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: I am using Tailwind classes here for styling. The &lt;code&gt;backdrop:bg-black/50&lt;/code&gt; class automatically dims the background of your app when the dialog opens!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 2: The Javascript Override (The Magic)
&lt;/h2&gt;

&lt;p&gt;By default, when Turbo sees &lt;code&gt;data-turbo-confirm&lt;/code&gt; on a button, it calls the browser's &lt;code&gt;confirm()&lt;/code&gt; method. We need to tell Turbo to use our new HTML dialog instead.&lt;/p&gt;

&lt;p&gt;Open your main Javascript entry file (&lt;code&gt;app/javascript/application.js&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;Turbo has a built in method called &lt;code&gt;Turbo.setConfirmMethod()&lt;/code&gt;. This method expects us to return a Javascript &lt;code&gt;Promise&lt;/code&gt; that resolves to either &lt;code&gt;true&lt;/code&gt; (if the user clicks Confirm) or &lt;code&gt;false&lt;/code&gt; (if they click Cancel).&lt;/p&gt;

&lt;p&gt;Add this code to your 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="c1"&gt;// app/javascript/application.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;@hotwired/turbo-rails&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="nx"&gt;Turbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setConfirmMethod&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&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;// 1. Find our dialog and text element&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dialog&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turbo-confirm-modal&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;messageEl&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turbo-confirm-message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Insert the dynamic message from the button&lt;/span&gt;
    &lt;span class="nx"&gt;messageEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Show the modal&lt;/span&gt;
    &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showModal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Handle button clicks&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;btnAccept&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turbo-confirm-accept&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;btnCancel&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turbo-confirm-cancel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// We use the &amp;lt;dialog&amp;gt; built-in 'close' method and pass it a string value&lt;/span&gt;
    &lt;span class="nx"&gt;btnAccept&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accept&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;btnCancel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancel&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;// 5. Listen for the dialog to close. &lt;/span&gt;
    &lt;span class="c1"&gt;// This catches both button clicks AND if the user presses the 'ESC' key!&lt;/span&gt;
    &lt;span class="nx"&gt;dialog&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;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="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;returnValue&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accept&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;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// 'once: true' automatically removes the event listener after it runs&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 code is brilliantly simple. We don't have to intercept form submissions or worry about race conditions. We just hold the Turbo request in a Promise until the dialog closes.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: Using It In Your App
&lt;/h2&gt;

&lt;p&gt;That's it! You don't need to change how you write your Rails code. &lt;/p&gt;

&lt;p&gt;Anywhere in your app, you can use the standard &lt;code&gt;turbo_confirm&lt;/code&gt; syntax. Turbo will automatically route the message into your beautiful new modal.&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="c"&gt;&amp;lt;!-- A standard delete button --&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;"Delete Project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;project_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@project&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;"btn-danger"&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;turbo_confirm: &lt;/span&gt;&lt;span class="s2"&gt;"Are you sure you want to delete this project? This cannot be undone."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- It also works on links --&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;"Sign Out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destroy_user_session_path&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;turbo_confirm: &lt;/span&gt;&lt;span class="s2"&gt;"Ready to leave?"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In modern web development, details matter. The default browser alert screams "amateur side project." &lt;/p&gt;

&lt;p&gt;By combining:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The native HTML5 &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element.&lt;/li&gt;
&lt;li&gt;A Javascript &lt;code&gt;Promise&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Turbo's &lt;code&gt;setConfirmMethod&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We created a reusable, accessible, and easily stylable confirmation system in about 30 lines of code. No React, no heavy modal libraries, and no dirty Javascript hacks. &lt;/p&gt;

</description>
      <category>rails</category>
      <category>hotwire</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Rails 8: How to Auto-Generate Social Media Preview Cards</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Sun, 26 Apr 2026 23:11:25 +0000</pubDate>
      <link>https://forem.com/zilton7/rails-8-how-to-auto-generate-social-media-preview-cards-4i1d</link>
      <guid>https://forem.com/zilton7/rails-8-how-to-auto-generate-social-media-preview-cards-4i1d</guid>
      <description>&lt;p&gt;Very often I find myself sharing a link to my new Rails project on Twitter, Discord, or LinkedIn. But when I paste the link, the preview just shows a blank, boring gray box. &lt;/p&gt;

&lt;p&gt;If you want your SaaS to look professional, you need OpenGraph (&lt;code&gt;og:image&lt;/code&gt;) images. The problem is, if you have a blog with 500 posts, or a directory with 10,000 user profiles, you cannot design 10,000 images manually in Figma. &lt;/p&gt;

&lt;p&gt;You could use a third-party API service to generate these screenshots, but they usually charge around $29/month. As a solo developer, I hate adding unnecessary monthly subscriptions.&lt;/p&gt;

&lt;p&gt;We can actually build a dynamic screenshot generator directly inside our Rails 8 app using HTML, Tailwind, and a headless browser. Here is exactly how to do it in 5 steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: The "Card" View
&lt;/h2&gt;

&lt;p&gt;First off, we need to design what our image will look like. Instead of trying to draw images with complex Ruby image-magick libraries, we are just going to build a standard Rails webpage. &lt;/p&gt;

&lt;p&gt;Let's create a special route that renders a specific post as a card. The trick here is that standard Twitter/LinkedIn cards are exactly &lt;strong&gt;1200x630 pixels&lt;/strong&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="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:posts&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;member&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="ss"&gt;:card_preview&lt;/span&gt; &lt;span class="c1"&gt;# e.g., /posts/1/card_preview&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;Now, create a very simple layout just for these cards so they don't load your app's navbar or footer.&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="c"&gt;&amp;lt;!-- app/views/layouts/card.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;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.tailwindcss.com"&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;/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;"w-[1200px] h-[630px] m-0 p-0 flex items-center justify-center bg-slate-900 text-white"&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;yield&lt;/span&gt; &lt;span class="cp"&gt;%&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;And the view itself:&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="c"&gt;&amp;lt;!-- app/views/posts/card_preview.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;"p-16 w-full h-full flex flex-col justify-between"&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-8xl font-bold"&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;@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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center text-4xl text-gray-400"&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;By &lt;span class="cp"&gt;&amp;lt;%=&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;author_name&lt;/span&gt; &lt;span class="cp"&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;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(In your controller, just remember to render this with &lt;code&gt;render layout: 'card'&lt;/code&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 2: The Screenshot Engine (Ferrum)
&lt;/h2&gt;

&lt;p&gt;Now we have a webpage that looks exactly like our desired image. We just need our Rails app to "take a photo" of it. &lt;/p&gt;

&lt;p&gt;We will use the &lt;strong&gt;Ferrum&lt;/strong&gt; gem. We talked about Ferrum in my web scraping articles - it connects directly to Chrome without needing slow Selenium webdrivers. It is incredibly fast.&lt;/p&gt;

&lt;p&gt;Add it to your Gemfile:&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="s1"&gt;'ferrum'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;bundle install&lt;/code&gt;. &lt;em&gt;(Note: Your server will need Chromium/Chrome installed. If you deploy with Kamal, you just add it to your Dockerfile).&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: The Generator Service
&lt;/h2&gt;

&lt;p&gt;Let's write a plain Ruby object to handle the screenshot. We want to point Ferrum at our &lt;code&gt;card_preview&lt;/code&gt; URL, set the window size to 1200x630, and capture the image.&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/services/card_generator.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'ferrum'&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CardGenerator&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;capture&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;# Boot the headless browser&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Ferrum&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Browser&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;window_size&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;timeout: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Go to our special Rails view&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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;# Optional: Wait for any custom fonts or images to load&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_idle&lt;/span&gt; 

    &lt;span class="c1"&gt;# Take the screenshot and save it to a temporary file&lt;/span&gt;
    &lt;span class="n"&gt;temp_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Tempfile&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="s1"&gt;'card'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'.png'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;path: &lt;/span&gt;&lt;span class="n"&gt;temp_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;format: :png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;

    &lt;span class="n"&gt;temp_file&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;
  
  
  STEP 4: Attaching the Image (ActiveStorage)
&lt;/h2&gt;

&lt;p&gt;We do not want to boot up a headless browser every single time someone shares our link on Twitter. That will instantly crash your database and eat all your server RAM. &lt;/p&gt;

&lt;p&gt;We need to generate the image &lt;strong&gt;once&lt;/strong&gt; when the Post is created, and save it using ActiveStorage.&lt;/p&gt;

&lt;p&gt;Ensure your &lt;code&gt;Post&lt;/code&gt; model is ready:&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;has_one_attached&lt;/span&gt; &lt;span class="ss"&gt;:og_image&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, let's use a background job (Solid Queue) to generate and attach the image right after the post is saved.&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/generate_og_image_job.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GenerateOgImageJob&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;post_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&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;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# We use Rails routing helpers to get the full URL&lt;/span&gt;
    &lt;span class="n"&gt;url&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;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;url_helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;card_preview_post_url&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;host: &lt;/span&gt;&lt;span class="s1"&gt;'https://myapp.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Run our Ferrum service&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CardGenerator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture&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;# Attach the image to the post&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;og_image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;io: &lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&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;"og_image_&lt;/span&gt;&lt;span class="si"&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;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="ss"&gt;content_type: &lt;/span&gt;&lt;span class="s1"&gt;'image/png'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt; &lt;span class="c1"&gt;# Clean up the tempfile&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;Just call &lt;code&gt;GenerateOgImageJob.perform_later(self.id)&lt;/code&gt; in an &lt;code&gt;after_create_commit&lt;/code&gt; callback on your Post model.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 5: The Meta Tags
&lt;/h2&gt;

&lt;p&gt;The hard part is done! You now have a dynamically generated image attached to your database record. &lt;/p&gt;

&lt;p&gt;When a user visits the actual &lt;code&gt;show&lt;/code&gt; page of your Post, you just need to tell Twitter and LinkedIn where to find that image. &lt;/p&gt;

&lt;p&gt;Open your main application layout file and add the OpenGraph meta tags inside the &lt;code&gt;&amp;lt;head&amp;gt;&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="c"&gt;&amp;lt;!-- app/views/layouts/application.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- Other standard meta tags... --&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;@post&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;og_image&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;attached?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image"&lt;/span&gt; &lt;span class="na"&gt;content=&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;url_for&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="nf"&gt;og_image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&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;"twitter:card"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"summary_large_image"&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;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;That's pretty much it. &lt;br&gt;
Instead of paying a monthly fee to an external API, we used the tools we already have in Rails 8. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We created a &lt;strong&gt;1200x630 HTML view&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;We used &lt;strong&gt;Ferrum&lt;/strong&gt; to take a screenshot of that view.&lt;/li&gt;
&lt;li&gt;We used &lt;strong&gt;Solid Queue&lt;/strong&gt; to run it in the background.&lt;/li&gt;
&lt;li&gt;We used &lt;strong&gt;ActiveStorage&lt;/strong&gt; to save the file permanently.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your application links will now stand out perfectly on social media, and because you are building the cards using standard ERB and Tailwind, you have 100% control over exactly how they look.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Do you use dynamically generated images for your apps? Let me know your workflow in the comments! 👇&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>seo</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>OOP vs Functional Programming Explained for Dummies</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Sat, 25 Apr 2026 23:11:24 +0000</pubDate>
      <link>https://forem.com/zilton7/oop-vs-functional-programming-explained-for-dummies-3f6h</link>
      <guid>https://forem.com/zilton7/oop-vs-functional-programming-explained-for-dummies-3f6h</guid>
      <description>&lt;p&gt;I see beginner developers get completely overwhelmed by computer science jargon. You read an article about "Monads," "Polymorphism," or "Immutability," and you feel like you are not smart enough to be a programmer. &lt;/p&gt;

&lt;p&gt;If you browse tech Twitter or Reddit, you will see constant wars between developers arguing about whether &lt;strong&gt;Object-Oriented Programming (OOP)&lt;/strong&gt; or &lt;strong&gt;Functional Programming (FP)&lt;/strong&gt; is better. &lt;/p&gt;

&lt;p&gt;But what do these terms actually mean? If you strip away the fancy university words, they are just two different ways of organizing how data moves through your app.&lt;/p&gt;

&lt;p&gt;Here is the absolute simplest explanation of OOP vs Functional programming, without the computer science degree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mental Model: The Robot vs The Conveyor Belt
&lt;/h2&gt;

&lt;p&gt;Imagine you want to build a system that paints cars red. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Object-Oriented Way (The Robot):&lt;/strong&gt;&lt;br&gt;
You build a smart Robot called &lt;code&gt;Car&lt;/code&gt;. You give the robot some data (its color is blue). When you want the car painted, you press a button on the robot's back called &lt;code&gt;paint_red!&lt;/code&gt;. The robot reaches inside its own body, takes out its blue paint, throws it away, and puts in red paint. The robot has &lt;strong&gt;changed itself&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Functional Way (The Conveyor Belt):&lt;/strong&gt;&lt;br&gt;
You build a dumb conveyor belt. You put a blue car on the belt. It moves through a machine called &lt;code&gt;PaintRed&lt;/code&gt;. The machine does not change the original blue car. Instead, it destroys the blue car, and builds a &lt;strong&gt;brand new&lt;/strong&gt; red car at the end of the belt. The machine remembers nothing. It just takes inputs and spits out new outputs.&lt;/p&gt;
&lt;h2&gt;
  
  
  CONCEPT 1: Object-Oriented Programming (State)
&lt;/h2&gt;

&lt;p&gt;Ruby, Python, and Java are heavily Object-Oriented. The core idea is that &lt;strong&gt;data and the functions that change that data live together in the same box&lt;/strong&gt; (called a Class/Object).&lt;/p&gt;

&lt;p&gt;We call this "State." State is just a fancy word for "remembering things."&lt;/p&gt;

&lt;p&gt;Look at this Ruby code representing a bank account:&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;# Object-Oriented Programming (Ruby)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BankAccount&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:balance&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;starting_balance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;starting_balance&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;deposit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@balance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;my_account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BankAccount&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="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;my_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deposit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;my_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balance&lt;/span&gt; 
&lt;span class="c1"&gt;# Outputs: 150&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what happened. &lt;code&gt;my_account&lt;/code&gt; remembered that it had 100. When we called &lt;code&gt;.deposit(50)&lt;/code&gt;, it changed its own internal memory. The original &lt;code&gt;100&lt;/code&gt; is gone forever. This is called &lt;strong&gt;Mutation&lt;/strong&gt; (changing things in place).&lt;/p&gt;

&lt;h2&gt;
  
  
  CONCEPT 2: Functional Programming (Immutability)
&lt;/h2&gt;

&lt;p&gt;Elixir, Haskell, and Clojure are Functional languages. The core idea here is that &lt;strong&gt;data and functions are completely separate.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Functions are just dumb calculators. They don't remember anything. You hand them data, and they hand you back &lt;strong&gt;new&lt;/strong&gt; data. &lt;/p&gt;

&lt;p&gt;We call this "Immutability." It is a fancy word for "you are not allowed to change the original data."&lt;/p&gt;

&lt;p&gt;Look at how a bank account works in a functional language like Elixir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Functional Programming (Elixir)&lt;/span&gt;
&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;BankAccount&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;deposit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;current_balance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;my_balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="c1"&gt;# We pass the data INTO the machine. &lt;/span&gt;
&lt;span class="c1"&gt;# It spits out a brand new value.&lt;/span&gt;
&lt;span class="n"&gt;new_balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BankAccount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deposit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;new_balance&lt;/span&gt; 
&lt;span class="c1"&gt;# Outputs: 150&lt;/span&gt;

&lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;my_balance&lt;/span&gt;
&lt;span class="c1"&gt;# Outputs: 100 (The original data was never touched!)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the difference? The &lt;code&gt;BankAccount&lt;/code&gt; module doesn't "hold" any money. It just knows how to do the math. We had to pass &lt;code&gt;my_balance&lt;/code&gt; into it, and it gave us a completely new variable back. &lt;/p&gt;

&lt;h2&gt;
  
  
  CONCEPT 3: Why does this matter?
&lt;/h2&gt;

&lt;p&gt;If both methods get the job done, why do people fight about them?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why people love OOP:&lt;/strong&gt;&lt;br&gt;
It is very easy for human brains to understand. We live in an object-oriented world. A dog barks. A user logs in. A bank account receives a deposit. It organizes your code into neat little real-world nouns. This is why frameworks like Ruby on Rails are so incredibly fast for building products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem with OOP:&lt;/strong&gt;&lt;br&gt;
Because objects "remember" things and change themselves, bugs can be incredibly hard to track down. If your &lt;code&gt;user&lt;/code&gt; object has the wrong email address, you have to guess which part of your massive app accidentally changed the email. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why people love Functional Programming:&lt;/strong&gt;&lt;br&gt;
It is incredibly safe and predictable. If a function always spits out &lt;code&gt;4&lt;/code&gt; when you give it &lt;code&gt;2 + 2&lt;/code&gt;, testing it is ridiculously easy. Furthermore, because data is never changed in place, you can run 10,000 functions at the exact same time (Concurrency) across multiple CPU cores, and they will never accidentally overwrite each other's data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem with FP:&lt;/strong&gt;&lt;br&gt;
It can be hard to learn. If you want to change a user's name nested deep inside a database record, you have to write code that makes a copy of the database, makes a copy of the user, and changes the name on the copy. It feels like a lot of extra steps for simple tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Don't let the jargon scare you. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;OOP&lt;/strong&gt; = Nouns. Data and functions live together. Objects change themselves.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Functional&lt;/strong&gt; = Verbs. Data and functions are separate. Data is passed through an assembly line to create new data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are building a standard web app (SaaS, blog, e-commerce), Object-Oriented programming is usually the fastest way to ship. If you are building a chat app that needs to handle 5 million messages a second without crashing, Functional programming is your best friend.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>beginners</category>
      <category>ruby</category>
      <category>career</category>
    </item>
    <item>
      <title>The Developer's Hardware Wallet: A Review of the OneKey Classic 1S</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Fri, 24 Apr 2026 23:11:26 +0000</pubDate>
      <link>https://forem.com/zilton7/the-developers-hardware-wallet-a-review-of-the-onekey-classic-1s-4mb1</link>
      <guid>https://forem.com/zilton7/the-developers-hardware-wallet-a-review-of-the-onekey-classic-1s-4mb1</guid>
      <description>&lt;h1&gt;
  
  
  The Developer's Hardware Wallet: A Review of the OneKey Classic 1S
&lt;/h1&gt;

&lt;p&gt;Recently I uploaded a &lt;a href="https://www.youtube.com/watch?v=K4lD0cI6W8g" rel="noopener noreferrer"&gt;new video&lt;/a&gt; reviewing a piece of hardware. If you are a developer, a freelancer, or just someone building in the Web3 space, your digital security is your most valuable asset. &lt;/p&gt;

&lt;p&gt;Very often I see developers who are incredibly smart with their code, but they are still keeping their funds in browser extensions (like MetaMask) or on centralized exchanges. That is a massive single point of failure.&lt;/p&gt;

&lt;p&gt;I have been looking for a good hardware wallet, and I recently got my hands on the &lt;strong&gt;OneKey Classic 1S&lt;/strong&gt;. It has been gaining huge traction in the developer community. Here is my honest review of why this device makes a lot of sense for coders.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The "Open-Source" Advantage
&lt;/h2&gt;

&lt;p&gt;As developers, we hate black boxes. If I am going to trust a piece of plastic to hold my money, I want to know exactly what the software inside it is doing.&lt;/p&gt;

&lt;p&gt;This is where OneKey beats the competition. Their firmware is &lt;strong&gt;100% open-source&lt;/strong&gt;. You do not have to just "trust" the company. You can literally go to their GitHub repository, look through the folders, and audit the code yourself. This is a massive advantage over some of the older, bigger names in the crypto industry that keep their code completely locked down and hidden.&lt;/p&gt;

&lt;p&gt;Under the hood, it uses an EAL 6+ certified secure element. Your private keys never touch the internet. Even if your laptop is infected with terrible malware, your assets are physically isolated inside this device. Hackers cannot physically press the "Confirm" button on your desk.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Design and Form Factor
&lt;/h2&gt;

&lt;p&gt;The design of the Classic 1S is incredibly minimal. It is about the exact size of a credit card and only 3mm thick. &lt;/p&gt;

&lt;p&gt;It feels very sturdy. You can plug it into your computer and it powers up instantly via USB-C, or you can connect it to your phone via Bluetooth. It doesn't feel like a clunky USB thumb drive from 2010; it actually feels like a modern piece of tech. &lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Software Experience
&lt;/h2&gt;

&lt;p&gt;Usually, hardware wallets have terrible desktop software. You buy a nice physical device, but the app you use to manage your coins looks like it was built 15 years ago.&lt;/p&gt;

&lt;p&gt;The software experience is where OneKey really shines. The OneKey App is an all-in-one hub. It is clean, fast, and supports over 100 chains and 30,000 tokens right out of the box. You don't have to manually install "apps" for every single different coin you want to hold.&lt;/p&gt;

&lt;p&gt;But for me, the killer feature is the built-in integrations. You can do instant cross-chain swaps, and they even integrated 0-fee Perpetual trading via Hyperliquid directly inside the app. You can do complex crypto actions without ever exposing your private keys to a random website.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;If you hold more than $100 in crypto, you really shouldn't be using a hot wallet. The peace of mind of having physical hardware is worth the small investment. &lt;/p&gt;

&lt;p&gt;The Classic 1S hits that perfect "sweet spot." It has high-end, open-source security, but it comes at a price point that actually makes sense for solo developers and freelancers.&lt;/p&gt;

&lt;p&gt;OneKey sent this unit over for me to test, and I have been genuinely impressed with their developer-first approach. &lt;/p&gt;

&lt;p&gt;If you want to pick one up and upgrade your security, I have an exclusive &lt;strong&gt;10% discount link&lt;/strong&gt; for my readers. &lt;/p&gt;

&lt;p&gt;🛡️ &lt;strong&gt;Get 10% off the OneKey Classic 1S here:&lt;/strong&gt; &lt;a href="https://onekey.so/r/6T9QXC" rel="noopener noreferrer"&gt;Check It Out&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stay secure, keep building, and let me know in the comments what hardware wallet you are currently using!&lt;/p&gt;

</description>
      <category>cryptocurrency</category>
      <category>security</category>
      <category>web3</category>
      <category>review</category>
    </item>
    <item>
      <title>Laravel for Rails Devs: How to Learn the Sister Framework in a Weekend</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Thu, 23 Apr 2026 23:22:41 +0000</pubDate>
      <link>https://forem.com/zilton7/laravel-for-rails-devs-how-to-learn-the-sister-framework-in-a-weekend-j1n</link>
      <guid>https://forem.com/zilton7/laravel-for-rails-devs-how-to-learn-the-sister-framework-in-a-weekend-j1n</guid>
      <description>&lt;h1&gt;
  
  
  A Rails Developer’s Cheat Sheet to Learning Laravel
&lt;/h1&gt;

&lt;p&gt;Very often I find myself talking to developers who think Ruby on Rails and PHP are from completely different planets. &lt;/p&gt;

&lt;p&gt;But if you are a Rails developer and you look closely at &lt;strong&gt;Laravel&lt;/strong&gt;, you will notice something funny. It looks incredibly familiar. That is because Laravel's creator, Taylor Otwell, openly admits he was heavily inspired by Ruby on Rails. &lt;/p&gt;

&lt;p&gt;Laravel is basically the Rails of the PHP world. It has the same MVC structure, the same active record pattern for the database, and the same focus on "Developer Happiness". &lt;/p&gt;

&lt;p&gt;If you ever need to work on a Laravel codebase, or if you are just curious about the other side of the fence, you do not need to learn everything from scratch. You just need a translation guide. &lt;/p&gt;

&lt;p&gt;Here is exactly how Laravel concepts map to your Rails brain.&lt;/p&gt;

&lt;h2&gt;
  
  
  CONCEPT 1: The CLI (Artisan vs Rails)
&lt;/h2&gt;

&lt;p&gt;In Rails, you do everything through the &lt;code&gt;bin/rails&lt;/code&gt; command. You generate models, run migrations, and open the console.&lt;br&gt;
In Laravel, the command line tool is called &lt;strong&gt;Artisan&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Start the server:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Rails: &lt;code&gt;rails server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  Laravel: &lt;code&gt;php artisan serve&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Open the console:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Rails: &lt;code&gt;rails console&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  Laravel: &lt;code&gt;php artisan tinker&lt;/code&gt; (Tinker is an amazing REPL built on PsySH)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Run migrations:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Rails: &lt;code&gt;rails db:migrate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  Laravel: &lt;code&gt;php artisan migrate&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you want to generate a model and a migration at the same time, Laravel has a great shortcut flag (&lt;code&gt;-m&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;&lt;span class="c"&gt;# Rails&lt;/span&gt;
rails generate model Post title:string body:text

&lt;span class="c"&gt;# Laravel&lt;/span&gt;
php artisan make:model Post &lt;span class="nt"&gt;-m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: Laravel doesn't generate the table columns from the command line by default. You open the generated migration file and write them in PHP.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CONCEPT 2: Dependency Management (Composer)
&lt;/h2&gt;

&lt;p&gt;In Ruby, we use &lt;code&gt;Bundler&lt;/code&gt; and a &lt;code&gt;Gemfile&lt;/code&gt;. &lt;br&gt;
In PHP, the standard package manager is &lt;strong&gt;Composer&lt;/strong&gt;, and the file is &lt;code&gt;composer.json&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;# Rails: Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'stripe'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Laravel:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;composer.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"require"&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;"stripe/stripe-php"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^10.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To install packages, instead of running &lt;code&gt;bundle install&lt;/code&gt;, you run &lt;code&gt;composer install&lt;/code&gt;. Simple as that.&lt;/p&gt;

&lt;h2&gt;
  
  
  CONCEPT 3: The ORM (Eloquent vs ActiveRecord)
&lt;/h2&gt;

&lt;p&gt;This is where you will feel most at home. Laravel uses an ORM called &lt;strong&gt;Eloquent&lt;/strong&gt;, which uses the exact same Active Record design pattern as Rails.&lt;/p&gt;

&lt;p&gt;Your models live in &lt;code&gt;app/Models/&lt;/code&gt; (instead of &lt;code&gt;app/models/&lt;/code&gt;). Querying the database looks almost identical, just with PHP syntax (arrows &lt;code&gt;-&amp;gt;&lt;/code&gt; instead of dots &lt;code&gt;.&lt;/code&gt;, and semicolons at the end).&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;# Rails&lt;/span&gt;
&lt;span class="vi"&gt;@users&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;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;active: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;created_at: :desc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&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;find&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Laravel&lt;/span&gt;
&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active'&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="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'desc'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&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;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relationships are also very similar. Instead of macros like &lt;code&gt;has_many&lt;/code&gt;, you define a method returning the relationship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Laravel: app/Models/User.php&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;posts&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CONCEPT 4: Routing and Controllers
&lt;/h2&gt;

&lt;p&gt;In Rails, all your routes are clumped into &lt;code&gt;config/routes.rb&lt;/code&gt;. &lt;br&gt;
Laravel splits them up. Web routes (which have sessions and CSRF protection) go in &lt;code&gt;routes/web.php&lt;/code&gt;. API routes go in &lt;code&gt;routes/api.php&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;# Rails: config/routes.rb&lt;/span&gt;
&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/about'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'pages#about'&lt;/span&gt;
&lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:posts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Laravel: routes/web.php&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/about'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;PagesController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'about'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PostController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Controllers are practically identical. They receive a request, ask the Model for data, and return a view.&lt;/p&gt;

&lt;h2&gt;
  
  
  CONCEPT 5: Views (Blade vs ERB)
&lt;/h2&gt;

&lt;p&gt;In Rails, we use ERB (&lt;code&gt;.html.erb&lt;/code&gt;) to mix Ruby into our HTML.&lt;br&gt;
Laravel uses a templating engine called &lt;strong&gt;Blade&lt;/strong&gt; (&lt;code&gt;.blade.php&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Honestly, Blade is fantastic. It is a bit cleaner than ERB because you don't have to type &lt;code&gt;&amp;lt;%= %&amp;gt;&lt;/code&gt; everywhere. You use the &lt;code&gt;@&lt;/code&gt; symbol for logic and double curly braces &lt;code&gt;{{ }}&lt;/code&gt; to output variables.&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="c"&gt;&amp;lt;!-- Rails ERB --&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;@user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_admin?&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;Welcome, &lt;span class="cp"&gt;&amp;lt;%=&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;name&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="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&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;Access Denied&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="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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Laravel Blade --&amp;gt;&lt;/span&gt;
@if ($user-&amp;gt;is_admin)
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Welcome, {{ $user-&amp;gt;name }}&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
@else
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Access Denied&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
@endif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CONCEPT 6: The Frontend (Livewire vs Hotwire)
&lt;/h2&gt;

&lt;p&gt;Rails 8 pushes &lt;strong&gt;Hotwire&lt;/strong&gt; (Turbo and Stimulus) to give you that fast, SPA-like feel without writing React. &lt;br&gt;
Laravel has an equivalent called &lt;strong&gt;Livewire&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;While Hotwire sends HTML over the wire by intercepting standard form submissions, Livewire actually ties PHP component classes directly to your HTML. When a user clicks a button, Livewire makes an AJAX request, runs the PHP method, and morphs the DOM automatically. &lt;/p&gt;

&lt;p&gt;Both accomplish the exact same goal: letting backend developers build highly interactive frontends without touching a massive Javascript build step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;If you know Ruby on Rails, you already know 80% of Laravel. You just need to get used to writing PHP syntax and adding semicolons at the end of your lines.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;Gemfile&lt;/code&gt; = &lt;code&gt;composer.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;bin/rails&lt;/code&gt; = &lt;code&gt;php artisan&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;ActiveRecord&lt;/code&gt; = &lt;code&gt;Eloquent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;ERB&lt;/code&gt; = &lt;code&gt;Blade&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Hotwire&lt;/code&gt; = &lt;code&gt;Livewire&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Laravel has an incredibly polished ecosystem (Forge for deployment, Nova for admin panels). While my heart still belongs to Ruby, I have massive respect for Laravel. It is proof that the "Majestic Monolith" is still the best way to build software, no matter what language you write it in.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>rails</category>
      <category>php</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
