<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://bridgetownrb.com/" version="2.2.2">Bridgetown</generator><link href="https://paagman.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://paagman.dev/" rel="alternate" type="text/html" /><updated>2026-10-29T08:55:09+01:00</updated><id>https://paagman.dev/feed.xml</id><title type="html">Paagman.dev</title><subtitle>Hi! 👋 I&apos;m Dennis Paagman, an independent, product focused developer. I&apos;ve been building Ruby on Rails apps for 15 years, and these days I help teams ship faster with AI agents.</subtitle><author><name>Dennis Paagman</name><email>dennis@paagman.dev</email></author><entry><title type="html">How fast can you actually patch Ruby?</title><link href="https://paagman.dev/how-fast-can-you-patch-ruby/" rel="alternate" type="text/html" title="How fast can you actually patch Ruby?" /><published>2026-10-27T00:00:00+01:00</published><updated>2026-10-27T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2026-10-27-how-fast-can-you-patch-ruby.md</id><content type="html" xml:base="https://paagman.dev/how-fast-can-you-patch-ruby/">&lt;p&gt;When I tried to update an app to &lt;a href=&quot;https://www.ruby-lang.org/en/news/2026/10/23/regexp-fixes-for-gitlab-cves/&quot;&gt;Ruby 4.0.9&lt;/a&gt; last Friday, the official Docker community image wasn’t available yet. The release fixed vulnerabilities GitLab had found, but I had to wait before I could deploy.&lt;/p&gt;

&lt;p&gt;A few months ago, a government Rails site was &lt;a href=&quot;https://rietta.com/blog/ruby-on-rails-cve-exploited-hours-after-patch/&quot;&gt;hit by an ActiveStorage vulnerability&lt;/a&gt; eight hours after the patch went out. Last week, &lt;a href=&quot;https://thephp.foundation/blog/2026/10/21/todays-php-security-release-and-how-were-strengthening-php-security/&quot;&gt;PHP2Shell&lt;/a&gt; was being exploited &lt;em&gt;before&lt;/em&gt; it was announced.&lt;sup id=&quot;fnref:php2shell&quot;&gt;&lt;a href=&quot;#fn:php2shell&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; Waiting for a Docker image can eat up the time you have to respond.&lt;/p&gt;

&lt;h2 id=&quot;when-ruby-409-became-available&quot;&gt;When Ruby 4.0.9 became available&lt;/h2&gt;

&lt;p&gt;I took every installer and version manager listed on the &lt;a href=&quot;https://www.ruby-lang.org/en/documentation/installation/&quot;&gt;official installation page on ruby-lang.org&lt;/a&gt;, then added container images, CI providers and hosting platforms I could think of. For each, I checked when 4.0.9 became available.&lt;sup id=&quot;fnref:times&quot;&gt;&lt;a href=&quot;#fn:times&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;All times start when the &lt;strong&gt;Ruby 4.0.9 source tarball became available&lt;/strong&gt;, on &lt;strong&gt;Friday 23 October 2026, 04:04 UTC&lt;/strong&gt;.&lt;sup id=&quot;fnref:tag&quot;&gt;&lt;a href=&quot;#fn:tag&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;h3 id=&quot;installers-and-version-managers&quot;&gt;Installers and version managers&lt;/h3&gt;

&lt;table class=&quot;timing-table&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Resource&lt;/th&gt;
      &lt;th&gt;Available after&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/rbenv/ruby-build/releases/tag/v20261023&quot;&gt;ruby-build&lt;/a&gt; (rbenv, and mise when compiling)&lt;/td&gt;
      &lt;td&gt;43 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/ruby/ruby-builder/releases/tag/ruby-4.0.9&quot;&gt;ruby/ruby-builder binaries&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;48 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/Homebrew/homebrew-core/pull/320592&quot;&gt;Homebrew ruby-build&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;1 h 10 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/asdf-vm/asdf-ruby/commit/83edf4358e773317ac200ce751874d3ec147f2e7&quot;&gt;asdf&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;2 h 43 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/jdx/ruby/releases/tag/4.0.9&quot;&gt;jdx/ruby binaries&lt;/a&gt; (mise’s precompiled Rubies)&lt;/td&gt;
      &lt;td&gt;8 h 15 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/oneclick/rubyinstaller2/releases/tag/RubyInstaller-4.0.9-1&quot;&gt;RubyInstaller&lt;/a&gt; (Windows, also RVM 4 Windows)&lt;/td&gt;
      &lt;td&gt;⚠️ 16 h 35 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/rvm/rvm/commit/4660dfc85e76686d9461455d2081d1884c226003&quot;&gt;RVM&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;⚠️ 18 h 34 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/spinel-coop/rv-ruby/releases/tag/20261023&quot;&gt;rv&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;⚠️ 21 h 40 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/postmodern/ruby-versions/commit/19a204db5575bc0109905bfbea0d3777948d3517&quot;&gt;ruby-install&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;⚠️ 5 d 10 h 46 m&lt;sup id=&quot;fnref:ruby-install&quot;&gt;&lt;a href=&quot;#fn:ruby-install&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/postmodern/chruby&quot;&gt;chruby&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;Depends on the installer used&lt;sup id=&quot;fnref:chruby&quot;&gt;&lt;a href=&quot;#fn:chruby&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/RubyMetric/rbenv-for-windows&quot;&gt;rbenv for Windows&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;❌ last updated to 4.0.1 in March&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;container-images&quot;&gt;Container images&lt;/h3&gt;

&lt;table class=&quot;timing-table&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Resource&lt;/th&gt;
      &lt;th&gt;Available after&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://hub.docker.com/r/rubylang/ruby/tags?name=4.0.9&quot;&gt;rubylang/ruby&lt;/a&gt; (by the Ruby core team)&lt;/td&gt;
      &lt;td&gt;8 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bitnami/containers/commit/6fb7b88e5c3c9210dc40a49503f9bd2085168676&quot;&gt;bitnami/ruby&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;1 h 59 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://gallery.ecr.aws/sorah/ruby&quot;&gt;sorah/ruby&lt;/a&gt; (by Ruby committer &lt;a href=&quot;https://github.com/sorah&quot;&gt;@sorah&lt;/a&gt;, uses sorah-rbpkg)&lt;/td&gt;
      &lt;td&gt;2 h 29 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/rails/devcontainer/pkgs/container/devcontainer%2Fimages%2Fruby&quot;&gt;rails/devcontainer&lt;/a&gt; (Rails dev container images)&lt;/td&gt;
      &lt;td&gt;9 h 34 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/docker-library/official-images/pull/22575&quot;&gt;ruby&lt;/a&gt; (by the Docker community)&lt;/td&gt;
      &lt;td&gt;⚠️ 15 h 57 m&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;ci&quot;&gt;CI&lt;/h3&gt;

&lt;table class=&quot;timing-table&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Resource&lt;/th&gt;
      &lt;th&gt;Available after&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/ruby/setup-ruby/releases/tag/v1.334.0&quot;&gt;GitHub Actions setup-ruby&lt;/a&gt; (Linux/macOS)&lt;/td&gt;
      &lt;td&gt;1 h 12 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/CircleCI-Public/cimg-ruby/pull/245&quot;&gt;CircleCI cimg/ruby&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;1 h 36 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/ruby/setup-ruby/releases/tag/v1.335.0&quot;&gt;GitHub Actions setup-ruby&lt;/a&gt; (Windows)&lt;/td&gt;
      &lt;td&gt;⚠️ 28 h 34 m&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;hosting-platforms&quot;&gt;Hosting platforms&lt;/h3&gt;

&lt;table class=&quot;timing-table&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Resource&lt;/th&gt;
      &lt;th&gt;Available after&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://devcenter.heroku.com/changelog-items/3850&quot;&gt;Heroku&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;2 h 28 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://docs.digitalocean.com/products/app-platform/reference/buildpacks/ruby/&quot;&gt;DigitalOcean App Platform&lt;/a&gt; (uses Heroku’s buildpack)&lt;/td&gt;
      &lt;td&gt;2 h 28 m&lt;sup id=&quot;fnref:digitalocean&quot;&gt;&lt;a href=&quot;#fn:digitalocean&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/jdx/ruby/releases/tag/4.0.9&quot;&gt;Railway&lt;/a&gt; (uses mise)&lt;/td&gt;
      &lt;td&gt;8 h 15 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Fly.io, Kamal and anything else using the official Docker community image&lt;/td&gt;
      &lt;td&gt;⚠️ 15 h 57 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://console.cloud.google.com/artifacts/docker/serverless-runtimes/us/runtimes-ubuntu2404/ruby/sha256:02a6f25828b8d42918b9683e777a3edc8b660a647bc257d41d090ca9f4588fbe&quot;&gt;Google Cloud&lt;/a&gt; (Cloud Run, App Engine)&lt;/td&gt;
      &lt;td&gt;⚠️ ≈ 3 d 14 h, default still 4.0.7&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://docs.aws.amazon.com/elasticbeanstalk/latest/relnotes/release-2026-10-27-al2023.html&quot;&gt;AWS Elastic Beanstalk&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;⚠️ 4 d 4 h 07 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/aws/aws-lambda-base-images/commit/2d1844bc7ad5525a67175e61aaccadfe93a413c1&quot;&gt;AWS Lambda&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;⚠️ 4 d 16 h 29 m&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;system-packages&quot;&gt;System packages&lt;/h3&gt;

&lt;table class=&quot;timing-table&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Resource&lt;/th&gt;
      &lt;th&gt;Available after&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://snapcraft.io/ruby&quot;&gt;Snap&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;7 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/sorah-rbpkg/ruby/releases/tag/debian%2F4.0.9-0nkmi1_noble&quot;&gt;sorah-rbpkg&lt;/a&gt; (APT packages for Ubuntu and Debian)&lt;/td&gt;
      &lt;td&gt;2 h 26 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/Homebrew/homebrew-core/pull/320602&quot;&gt;Homebrew&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;2 h 41 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/NixOS/nixpkgs/pull/577241&quot;&gt;nixpkgs&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;3 h 47 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://bodhi.fedoraproject.org/updates/FEDORA-2026-4b2e500f14&quot;&gt;Fedora&lt;/a&gt; (Rawhide; &lt;a href=&quot;https://bodhi.fedoraproject.org/updates/FEDORA-2026-3bacbb1eb5&quot;&gt;44&lt;/a&gt;/&lt;a href=&quot;https://bodhi.fedoraproject.org/updates/FEDORA-2026-f03a317854&quot;&gt;45&lt;/a&gt; still in testing)&lt;/td&gt;
      &lt;td&gt;⚠️ 11 h 03 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/microsoft/winget-pkgs/pull/460528&quot;&gt;winget&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;⚠️ 20 h 54 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Debian, Ubuntu, Alpine, Arch&lt;/td&gt;
      &lt;td&gt;❌ no Ruby 4 at all&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;what-stands-out&quot;&gt;What stands out&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The fastest ones are really fast, and mostly come from Ruby itself.&lt;/strong&gt; The Snap package and the &lt;code class=&quot;highlighter-rouge&quot;&gt;rubylang/ruby&lt;/code&gt; image are both published by the Ruby core team, and were available after 7 and 8 minutes. A bot committed the ruby-build definition 28 seconds after the tarball was uploaded, and the ruby-build release followed at 43 minutes.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The official Docker community image took almost 16 hours.&lt;/strong&gt;&lt;sup id=&quot;fnref:official&quot;&gt;&lt;a href=&quot;#fn:official&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;7&lt;/a&gt;&lt;/sup&gt; The Dockerfile Rails generates uses this image, so it’s also what you get with Kamal and Fly.io by default.&lt;sup id=&quot;fnref:pulls&quot;&gt;&lt;a href=&quot;#fn:pulls&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;8&lt;/a&gt;&lt;/sup&gt; The Dockerfile itself was updated about seven hours after the tarball appeared. Most of the wait came after that, in the &lt;a href=&quot;https://github.com/docker-library/official-images&quot;&gt;official-images&lt;/a&gt; review and build pipeline. That review serves a purpose, but it also adds hours to the wait for a security patch.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Some platforms are even slower.&lt;/strong&gt; AWS Lambda and Elastic Beanstalk are still on 4.0.7, &lt;del&gt;DigitalOcean’s docs list 4.0.1–4.0.2,&lt;/del&gt; and Google Cloud took about three and a half days to publish a 4.0.9 image, while its default for Ruby 4.0 is still 4.0.7. If you rely on a provider’s Ruby runtime, you depend on its update schedule. &lt;em&gt;Edit: Elastic Beanstalk and Lambda got 4.0.9 after 4 days. Rectification: DigitalOcean actually supports 4.0.9, as it uses Heroku buildpacks.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;how-to-patch-faster&quot;&gt;How to patch faster&lt;/h2&gt;

&lt;p&gt;Your version manager, CI and production environment all need to support the new version before you can roll it out. The slowest one decides how fast you can patch.&lt;/p&gt;

&lt;p&gt;Some things you can do before the next Ruby security release:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Check the whole path to production.&lt;/strong&gt; Find out how your local version manager, CI and production environment pick up new Ruby releases. Look for a manual approval, a scheduled build or another step that could hold up an update.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Automate the boring part.&lt;/strong&gt; Let a bot pick up new versions of your dependencies (Ruby, your gems and your base image), run CI and deploy when everything is green. The fewer manual steps, the faster you’re patched.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Choose a hosting setup that lets you patch promptly.&lt;/strong&gt; I’d avoid relying on a provider’s Ruby runtime if it’s still two patch versions behind. Use a custom container where supported, so you can update Ruby on your own schedule.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;most-of-this-is-volunteer-work&quot;&gt;Most of this is volunteer work&lt;/h2&gt;

&lt;p&gt;All of this puts extra pressure on the maintainers of these tools, and most of them are volunteers. Every patch release means a chain of PRs, reviews, builds and releases across dozens of projects. The expectations keep going up.&lt;/p&gt;

&lt;p&gt;The fastest ones show how much of that work bots can take over, so maintainers don’t have to drop everything for every release.&lt;/p&gt;

&lt;p&gt;So a big thank you to everyone who makes this happen, often within minutes. 🙏&lt;/p&gt;

&lt;h2 id=&quot;updates&quot;&gt;Updates&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;28 October 2026:&lt;/strong&gt; Added &lt;a href=&quot;https://github.com/rails/devcontainer&quot;&gt;rails/devcontainer&lt;/a&gt; to the container images (about 9.5 hours).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;28 October 2026:&lt;/strong&gt; AWS Lambda’s &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby:4.0&lt;/code&gt; image was updated to 4.0.9, about 4.5 days after the release.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;28 October 2026:&lt;/strong&gt; AWS Elastic Beanstalk updated its Ruby 4.0 platform to 4.0.9 on 27 October, about 4 days after the release.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;28 October 2026:&lt;/strong&gt; ruby-install’s version metadata was updated to 4.0.9, about 5.5 days after the release.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;28 October 2026 (rectification):&lt;/strong&gt; DigitalOcean App Platform uses Heroku’s Ruby buildpack and binaries, so it got 4.0.9 at the same time as Heroku, even though its docs only list 4.0.1–4.0.2. Thanks to &lt;a href=&quot;https://x.com/dblockdotorg/status/2115468311160897920&quot;&gt;@dblockdotorg&lt;/a&gt; for confirming.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;29 October 2026:&lt;/strong&gt; Added &lt;a href=&quot;https://github.com/sorah-rbpkg&quot;&gt;sorah-rbpkg&lt;/a&gt;’s APT packages for Ubuntu and Debian and its &lt;code class=&quot;highlighter-rouge&quot;&gt;sorah/ruby&lt;/code&gt; container image (both about 2.5 hours).&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:php2shell&quot;&gt;
      &lt;p&gt;Amazon’s honeypots saw &lt;a href=&quot;https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/&quot;&gt;the first attempts four days before disclosure&lt;/a&gt;. &lt;a href=&quot;#fnref:php2shell&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:times&quot;&gt;
      &lt;p&gt;Availability was checked through October 29. Most timestamps are Git commit dates or GitHub release dates. For Docker images I used Docker Hub’s last pushed date, or the GitHub Container Registry’s creation date for rails/devcontainer; for Heroku, the S3 &lt;code class=&quot;highlighter-rouge&quot;&gt;Last-Modified&lt;/code&gt; header. For Homebrew it’s the bottle commit. For sorah-rbpkg I used the GitHub release date of the Debian packages, and for its &lt;code class=&quot;highlighter-rouge&quot;&gt;sorah/ruby&lt;/code&gt; image the completion of the image manifest job, as the tags on ECR have since been re-pushed. For Google Cloud I used the upload time of the &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby:4.0.9&lt;/code&gt; runtime image in Artifact Registry. For AWS Lambda I used the creation date of the &lt;code class=&quot;highlighter-rouge&quot;&gt;public.ecr.aws/lambda/ruby:4.0.2026.10.27.20&lt;/code&gt; image, the first one where &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby -v&lt;/code&gt; reports 4.0.9. For AWS Elastic Beanstalk I used the &lt;code class=&quot;highlighter-rouge&quot;&gt;DateCreated&lt;/code&gt; of the Ruby 4.0 platform version 4.15.0. &lt;a href=&quot;#fnref:times&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:tag&quot;&gt;
      &lt;p&gt;The &lt;a href=&quot;https://github.com/ruby/ruby/commit/3e93b23afcec3433f6aecc34b5d832799d754238&quot;&gt;v4.0.9 tag&lt;/a&gt; was pushed at 02:36 UTC, 1 h 28 m before the tarball appeared on cache.ruby-lang.org. The announcement post followed 34 minutes after the tarball. The fixes themselves were public even earlier as regular commits in the Ruby repository, and some were already part of 4.0.8. I use the tarball upload to measure how quickly downstream tools packaged the release. &lt;a href=&quot;#fnref:tag&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:ruby-install&quot;&gt;
      &lt;p&gt;ruby-install gets its version list and checksums from &lt;a href=&quot;https://github.com/postmodern/ruby-versions&quot;&gt;postmodern/ruby-versions&lt;/a&gt;. This row tracks that metadata. ruby-install also &lt;a href=&quot;https://github.com/postmodern/ruby-install#readme&quot;&gt;supports explicit versions, custom source URLs and checksums&lt;/a&gt;, so you can supply the release tarball URL and its official checksum before the metadata is updated. &lt;a href=&quot;#fnref:ruby-install&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:chruby&quot;&gt;
      &lt;p&gt;chruby switches between installed Rubies and doesn’t require an update for each release. Its documentation covers &lt;a href=&quot;https://github.com/postmodern/chruby#rubies&quot;&gt;manual installation and using either ruby-install or ruby-build&lt;/a&gt;. With ruby-build, the release was available after 43 minutes; you could also compile the tarball yourself. &lt;a href=&quot;#fnref:chruby&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:digitalocean&quot;&gt;
      &lt;p&gt;DigitalOcean’s &lt;a href=&quot;https://docs.digitalocean.com/products/app-platform/reference/buildpacks/ruby/&quot;&gt;docs&lt;/a&gt; and &lt;a href=&quot;https://docs.digitalocean.com/release-notes/app-platform/&quot;&gt;release notes&lt;/a&gt; still only list Ruby 4.0.1–4.0.2. Its pinned Heroku buildpack downloads Ruby binaries from Heroku’s S3 bucket, where 4.0.9 appeared after 2 h 28 m, and &lt;a href=&quot;https://x.com/dblockdotorg/status/2115468311160897920&quot;&gt;@dblockdotorg confirmed&lt;/a&gt; that 4.0.9 deploys. &lt;a href=&quot;#fnref:digitalocean&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:official&quot;&gt;
      &lt;p&gt;What counts as ‘official’ is a bit confusing here. &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby&lt;/code&gt; is a Docker Official Image maintained by the Docker community, while &lt;code class=&quot;highlighter-rouge&quot;&gt;rubylang/ruby&lt;/code&gt; is published by the Ruby core team itself. &lt;a href=&quot;#fnref:official&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:pulls&quot;&gt;
      &lt;p&gt;Docker Hub shows ~1.59 billion pulls for &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby&lt;/code&gt; and ~3.5 million for &lt;code class=&quot;highlighter-rouge&quot;&gt;rubylang/ruby&lt;/code&gt;. The image that took 15 h 57 m has about 450 times as many pulls as the one available after 8 minutes. &lt;a href=&quot;#fnref:pulls&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;</content><author><name>Dennis Paagman</name><email>dennis@paagman.dev</email></author><category term="ruby" /></entry><entry><title type="html">Introducing Rebundler: make your Gemfile look great with a single command</title><link href="https://paagman.dev/introducing-rebundler/" rel="alternate" type="text/html" title="Introducing Rebundler: make your Gemfile look great with a single command" /><published>2026-08-04T00:00:00+02:00</published><updated>2026-08-04T00:00:00+02:00</updated><id>repo://posts.collection/_posts/2026-08-04-introducing-rebundler.md</id><content type="html" xml:base="https://paagman.dev/introducing-rebundler/">&lt;p&gt;Today I’m introducing &lt;a href=&quot;https://github.com/dennispaagman/rebundler&quot;&gt;Rebundler&lt;/a&gt;. It’s a little tool to automatically re-organize and annotate your Gemfile. I wrote it because I like my Gemfiles to be structured neatly, but in reality I always just put gems near vaguely adjacent gems or end up chucking them at the end of the file. Eventually my Gemfiles turn out to be an incoherent mess.&lt;/p&gt;

&lt;p&gt;Rebundler groups and sorts your gems and adds a description to each, which gives you more context on what a gem does. Especially with some of the funky names in the Ruby ecosystem, it’s not always clear from the name alone.&lt;/p&gt;

&lt;p&gt;Here’s a small example (full example below):&lt;/p&gt;

&lt;div class=&quot;my-6 lg:-mx-24 xl:-mx-40 2xl:-mx-64&quot;&gt;
  &lt;div class=&quot;grid gap-4 md:grid-cols-2 [&amp;amp;_pre]:max-h-[40rem] [&amp;amp;_pre]:overflow-y-scroll [&amp;amp;_pre]:m-0&quot;&gt;
    &lt;div class=&quot;flex flex-col gap-2 min-w-0&quot;&gt;
      &lt;div class=&quot;text-center text-sm text-gray-400 mb-1&quot;&gt;Before&lt;/div&gt;
      &lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://rubygems.org&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rails&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;puma&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;avo&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:development&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;herb&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;flex flex-col gap-2 min-w-0&quot;&gt;
      &lt;div class=&quot;text-center text-sm text-gray-400 mb-1&quot;&gt;After&lt;/div&gt;
      &lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://rubygems.org&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;avo&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Admin panel framework and Content Management System for Ruby on Rails.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;puma&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A Ruby/Rack web server built for parallelism.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rails&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Full-stack web application framework.&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:development&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;herb&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# The modern HTML+ERB Toolchain&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2 id=&quot;installation&quot;&gt;Installation&lt;/h2&gt;

&lt;p&gt;You can either add it to your Gemfile or run it standalone.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;bundle add rebundler&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;gem install rebundler&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;❯ bundle exec rebundle
Reordering and annotating Gemfile...
✓ Gemfile has been reordered and annotated
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There’s a ‘CI mode’, where it checks if the formatting is still correct, without writing to the file. Useful if you want to add it as a step in your linting pipeline.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;❯ bundle exec rebundle --ci
Checking if Gemfile is properly formatted...
✓ Gemfile is properly formatted
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;try-it-online&quot;&gt;Try it online&lt;/h2&gt;

&lt;p&gt;You can also play around with it on the online &lt;a href=&quot;https://rebundler.dev/&quot;&gt;Rebundler Playground&lt;/a&gt;. Just paste your Gemfile and hit Rebundle. It encodes the file in a hash in the URL, so you can easily share the URL around.&lt;/p&gt;

&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;/h2&gt;

&lt;p&gt;Since Gemfiles are just regular Ruby files, they can be parsed with Prism. Rebundler uses Prism’s visitor pattern to group gems that it thinks belong together. It also looks out for generic Gemfile setup methods, like &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;source&lt;/code&gt; and puts them at the top.&lt;/p&gt;

&lt;p&gt;Gems defined at the ‘top level’ are grouped together. Gems inside blocks (like &lt;code class=&quot;highlighter-rouge&quot;&gt;group :development do .. end&lt;/code&gt;) stay together, and duplicate groups are automatically merged.&lt;/p&gt;

&lt;p&gt;Prism is only a parser: it can’t format the parsed code back into Ruby&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. To work around this, Rebundler does some classic string manipulation. Prism saves the exact line and column coordinates of each parsed piece of code, so it uses those to extract the original code as a string. This has the added benefit that it keeps your code style intact, as the code is just copied.&lt;/p&gt;

&lt;p&gt;When reconstructing the Gemfile, it sorts each group’s gems alphabetically. It then loads each gem’s description from the local bundle (fetching it from RubyGems if it’s missing) and appends it to the line as a comment. To do this it needs to take some additional steps to find the trailing comment of the original line&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;By default it keeps existing trailing comments intact. Other comments are currently discarded.&lt;/p&gt;

&lt;p&gt;The current version should work with most Gemfiles, but there are probably esoteric ones out there which it doesn’t support (they are Ruby files, after all, so you can do anything you want). If you have a straightforward Gemfile with only ‘&lt;a href=&quot;https://bundler.io/man/gemfile.5.html&quot;&gt;Gemfile syntax&lt;/a&gt;’ it should work.&lt;/p&gt;

&lt;h2 id=&quot;full-example&quot;&gt;Full example&lt;/h2&gt;

&lt;p&gt;Here’s a full example using &lt;a href=&quot;https://github.com/rubyevents/rubyevents/blob/main/Gemfile&quot;&gt;RubyEvents’ Gemfile&lt;/a&gt; (&lt;a href=&quot;https://rebundler.dev/#M4ewrgTgxgpgBAIgBYBcUAdgC4D0OJgBGAngOYwC2wAdCBKQgFCkCWKA+qJLABRasokRAJRwA3nAA+EGOhCTEqDNjwChhalBAUcAYjEy5AX2oCEcI40YEScAGYsANjCyJqN4gFoAbjAjAWEAA7JkZdOABVYHgAJQBDJ2A4GAATcmZKRAgEx2AEABo4NSJXBGzE-By8q3CAFSR4ChAUvyC4OOBolDh0FnQYRxYg+Ds6OHjEuABtJUxcHGKNLR1y3Jx0CBBMJDi7FABdDIpEDa3gHb3Q8Kj4YABHQZQYAGZ2pMF4FLiUOMIOkbGAEEoCgWL5xjAtBAUkdEPdHi8CogAHwAXjgACZqABGagABiukWicA+cAACmAKHE4AB3GCEODRCC+CDTWYqBZsdSabTrSlxPlUw7kY4IdD8wlgYkAK0IYCcLVZo1ZpLivVhCFl8scisJACEwEEUs52kaSdkgsBeiaAFJxbxxADKUAgfW6MzQc1UXKIPJWVRw0uAcqNgyCpE8q2Ah3CIsQQZDxqGEajhIAEiAUDSWDIAORJR1kwGeQYAa3g6Di5HaUFgzmyKDGHuU8xQkEIIGoSEz2ZkKWoLW8wsyCDbEA7kaq6Z7OZg+bgTRawG6dodztd6G6dmyFBgNLopbZno5y5YFDAjilXZnfYHMCHYSKI9P58vwEniX1huN8DiZtOsCdHAADCjqOkeLbeoIvrLJUFRQJ0iZhimVQxk+ooIcG37IR+uSEjcEIpCwSRxF8m5+CSIBwAQbTAqCwQgb8JpDD0mwpGAIKBEEj5xmUqTEUiCBonAAAs+I4vhxIANJ9sRlFPt0SAsKQDQQCW94DHAXw-CSxD9EkLExPxSTNl6nLQUsvJRjgpaydGPEjrZxmSfAdFgvAACyzSaTswCcJCkAwOwladPu0IQWZpDyku7hEMQwRRrQ9A4HEnG+Owi4DOwfwBAhXYoBQji6NEUCBSFwBhSkw6ioQLp6SggkAH7InAzw4tQADshIAOpDCkIA0kk-UwEkQSZnAQxQJeLRwAAXsEMBDKM9hOCNhSgHASHwKSKCzUtICeNp1Iihqu37Yd3xxEi6CON8ypUK4ACkLBTNmRoDUk0oeIcj5GexgGbSA42gru7xIJsYAqXAUCpUp4YANzUTAdzyn2E1tFoQQOKQOAdpm7iEBqeMoMAQRqkiMgo7Orh2HEuQwDUcAACKyI4IDECSSkkegN0sDD9FtH+xA0qpv4kczIBQOWrKYz8QwUaZHKlnEVKOIdrPs0lpDVYgyuq4JWIdfiFPI6jLj2HT0SM4CKQpHAaa1LUZJvF00Ow8mODLBsI0BAxf52wAGp4jowEaDgmqldZ+N8XHyRSVIRRyix+rj-wwxQ6A4IIBDLn4OA66O4NSk8EAm1TMg05bDOPgRbngo6jbZNWDqun+JOJ-MUUsDFHgJVUWspWlQXLnQVZBSALLeCwe75YVugoBawD3cmnhnmP9m8Wv5DBZsgEBOGTUtbiGKEgAklS1YAT7yb2GMGlBN0HSu+QwwNlxGoUEMLAZVWfOloSgJHCOEZHQduIA7BwClDAOwF50b3Rjn7DsYBugaQgBzLQhoF4c0rFLMedsOjQ2CL4II08H5wEeCgE0GCH5oLgCAQg0pIQkw1NQhe09qiPlPo6AA8nAAAbM8AAnJ4bEpo7ZcN4QI4RGJyF-iimPQhM1Q5sJGmIwhQRiGkNgAuGAghmjAA1MRA6UiAELi-qraGgxlF0IgXqS8I1SzEBwICJ2mxGxwEBGSU+H8v7AEcYfOAeJqBiQJIzAAogADwXqlbowEQBAN+KPRucBuEMKYUkbc2g4DxUgB4oeEIoQpGAIUakwwaTqNgJuN2QDUgeM6JLFg3xampMYSCAxvEYnuXYDIQpngn4NKaSkdg9DWkNUZmmGApFnBAWAuDbQnxXQslvqyYCapiB-GyCwsAGw2AwFMfvUgJoWiDBUigGBwCYhxVpHEDm7iwqHmzIIDxp9qAag8OwIBFAAm4mxAAVmNtbRkZ4bq-jNFNSZbRLm2CZo6AAMss6GMgY7hjgDaHhAA5RkUAGhUhoG8uKHzHAUE8MAbFlArqFEWKUKk0A6CZiQPgAlnySVkqpEiQgFpsWlExkRAWltCRou4Zi50OLqQOkGNpOgGogzBE4KyvwhIACa4BahEHgAANVeJ40+ljSEoFebxYgYzHwADEVZOA5uA8hLAOVxFdKowQ3xGTbLkBAboLdAhSmWVSMBECA4eXhQgOwMBUh5ANSOfweRCgIGaoE6gbVsRKsBAGuAYTeU339hCWmII6A30eUgOAepdElzyaVbIUBiAamIH4P4ASgn-MTYzMkA0-DnLUdEFW0ykgOwDX0mkdr4BhJiHqHodqDlqMbPE5M4bRSqUIHW6ggiAG0U4sEdV08aSeC9jHQgJoh0jtDqwYYtIuQLm8hANoTMA5wE8EjVeF95Z23zfbGtM6siTNXRojdC7ngANIGNU8UBR1HoQW0FiN0EhtA8G+sUVZiCEg6UPHpdAUieFDjsIIsAikPF2c8PpZES5MF4qACV7AYZksJEzS6OUYCeD+FLWpdd4A2noZtVK5YjQwZI93dgKMYBgD2ZSn0hBSjShAOcZwu6-xQBwNxoZfGBPss5UgUoHRiCYc8JlQk3D+hv2CHTdRC94kZKBPkljhNeKf06FxMjwQjNqzE4QDh4RHTAucIUOQdIIDnMKA4fwKBPCVjdRzP8dMjV8xM6yCYuQNRxG7MQH+Tx4OM3qLcNzI1uj9tuVRUgEN0AkkoK6gzR0NS5fAOgbSezGbASRU8TakzkEsDbauJ0Lo3TQx2G6pIz6FrWuPVaqFlbeLYrtSgUsf8An-NCTXYk666uGSqGojlN8xPZeyeAVkLX1ztZgBE3TWiGa8Snk8bpU5qvxOcJ+mxcBHRhN4Q0RwunIvjCuQxaLeLLO6LiJ4H4pBnNwFhSAP7PRW10CpJh0FdtduVIFlpS69hNjHByayNU+X3Go4CP+umfpYs80x2TRwADkHxJAKRLSmZQ7eHRuMfuGp+pPA0bhf7gJic+r5k+V+oHruoEKn10sBk2hPCie0iNVR2BxGJ2Gf+0bY0-MJGEjoFqOfRzq-6+FrmnhUkwBqAImu1TsBfirugAS+HxtMduFWe4DwIrlAqG+MgpRMRgIUJ4y4ndwAAGTJEw2qYAF5BlwCnnuQhGcFoPxF6KIPNJbOh+GA-ABtskhMaMr0q0kImvs93Hoop8kmZoKwFgU+JDQR0xYLNPwdO0Gr2L40wY5e1KphS3pW4xBc7HGVC92wfueagJvloPwUAuLFPUcuco4e1Fe2cBE3S+lK9eBQM3qNiBZedUJGSNxkt4mFrAHYYN-h2gyDgAAcSBocucQ14dDBLiN8MAIIA+pg6QU-zhAsb7lHYMuZtK702tkPczBeyRQd78SJE9OZ4AAADXTe6YAcAhcVKTYeSV2bZOARzJIWwTGYhAWGDTpdKRzQLIAqgFyMAlJfoIIQEHVbVE9J5QbAAQnxRIE8C2FDgSEJCMkrBzDQM2A41RQxUZDYXDA+xHBlSCEjFkASFLhlxaiCSxGm3CA8jtVLH6hpEFx2G6GAF3CAVGj5kOwjVSBhggH6AakkNamoD4UJEdHUx+GnyUhUhOVQGTDeXAHIACTEhEkJDiUwVoRDku26DJEvCPRYXABoQS2iB8IT0-jaErGGGAQt13HuTUTiQfmsXkLJhFGsQsLbwRUGzoUhVpw6W8BAFCDK2QKwHtBAEKC2D5UcFcAXgE3J0YDgHQkQDKNf0KPWg21gFKHZHmBwWVnIAgDvG8BSgKM8CQDuBwCYFDhhEfHkIgH50-ieTkmpCPzYDTCIBJQXxNBpUUIGjaH4mSQ72yLe0W1R0GH5iHxg22PUK5HNw6BQHWl2Gd2SCiVDgCF3QrDHQog7ziQoCaCCFmNLGoDgHqDkhpGyEwGIOWGyEPEuWXERSaRg2WD+O2IVWjWEixFN0bUfGT0hBQ2tXLHRhLmzW2ioiRTtmCEcBC1rB9jUT41oTd1BCA0VWTXhXDhGg1EyXLyCG6VxOhAXQxCNhPkBQwNDj1WuTWyIl3z4NdBRRYg8DpyazsGS2xKHhTzxNAD2HVmcFd3J2PVdFOQVNJTtRSEJHkPLCSExj9wiNICRmXDbTpAZCZCngBhaDJ0mWAGIBg1mFG2VOuGJG4QoBIRZyeXcW71dW6FfFBFaKnkVHaGQQaAfj5i50Vk7mExTm0BIQl0EBwAzMaXjILlzKzKQG013z5lrxSUDJYGDILVHyaVIA5g7xWJQDWI0A1ELPjM8EWDX02BjNUWpAWOUi5yrEgzhOAnVTCU8AxDxD+U8EEQxAAA4RIO4oJuRYItAQBSxKwUgczKyiymcyNgBvMd4KdP0CzdyOyowDyjzTgnhP1l0aw94bUI5idJ9dE79O9LVcjJgMdIFx0MwsxZwvSbwgplgOwOwIkF0Ql3DtA-jyFJY6ZPh4dM1olLRboBZnt3sJpsR5yggYMoxV5sKQhjD5yAVfork3cjRjSkgRQI9EAmDLRgBCc4BwhuiVyYIrI4oczSDOhHAcBiI-cRocBBERJBFqtbogJ3FbcdRoZi4skjppSOIxw1ogVP5boVQqJqQ0wOgkAYNxNaiQRoKM5tT4ByBN8iIUUSNGsGJDi4pH8YAzLUSihhNVMp8ZBiJX4Fh7KtBdRmKiR4A0KMsnwHLWRn10VtAhgY5jhgUezKBlEkhMEnA4BhgokkZnB-hGZBsj9KAaA6AbTKCwTUcvixg4x9p79QNStsrTFDkVY6UvVgJYUdVJ1HBxstwxhBtStKQf0JIMqYByBp9Kw0BWhGRJloA4YbSWI2TaKEBctZBtx5RjVwgatJk6tCAGtQRmt7RWsNwIzfcqCC1eswx4ABs4oP41RxspZIx51jDpDSK6hF9XjYgrkpo9VTp7rLRBNl8WoxIsS7r9IHrCTeq9NBcqJkcLYCjXQ6tuEYgPJXq-r3qmdvlxIgloA3CrBij8ssBBwBgthdwH5CgsBXduh+oGjfKQ54BWKFhooRpYoSA+5EgB4Wg5RSAj1SBTtEhxceYzjQMaAecipGbIYWbPB80fsGh1YmbOzKASbeIto1IJdeVP9qYLYf9GjeJ+aGAPM0LoCnoXoKBXQT13pBpDgVaRwSB6RIYmBjbRQIjdllxDpFSfhAN+TJijQ0a8s4BMaNI2Z0BcaiaQASbwhAQmiO9Dc34UU-wxofh0KsjFtThRk8VLbmiggI6mlxxQhGi-T4BLT4kjq9iIkYch9gNVEUy2LLJ-QKgHSt1ggSMYAjamiEAK6s7nA07fKbY7Y09ak-g0gi6Kbk5YIPIv519wFVoIBKgLrrbWi2SIBa7eJy1SxNMv4J7h7m7wgTUxgYYgENj2NWIh7kJ7Bbpdxcs1QkADEE6EA3cpZTgP8rB06bt+halkCkT-YkgGJGKBo4DsV5YkgcBNplJ2g8dlzzJVyrIAwrQZTtZ-a67QHkxl6-LrVc5gbrkUBsUVppkEUb9yATIe60zYJ5FoQcBBh4Hp6RwCGGcAk2pfkYGj8wBjSEV+0kGxqUHM6MMMG1EsyDpgVe8UVi7AH2KdBcHtz+GiHRR+GYGi1MMkBtiJpRoqI2Zb9WRJpppakWJSQKKvhwpBgGRb0oE7YO9VrxHtiHDT69HsVtjm6Z64oSwrQEbChKYv8larZT7VHjTU7jDcQCQbHTZFbaZla66a0Pkr8FaK57GGZT6izlEkyBZPB682ikZy5zZvGrYpjXbyt3bCb6ib6CIPTMjCab5uGu4e44o6bcgB4cnwxZ4iosnNcfsMtkwhHEAYZ0B1k7UropbRdEhK6aELs-BqnTwD5WnRRQjRTKQhb6QUhFkFV+nEBvAXQTdurT6HSmgpZnbpjN516pi7UAksRsQT4gA&quot;&gt;view in playground&lt;/a&gt;). It’s a good example of a ‘messy’ Gemfile, with occasional comments, commented-out gems, and no real grouping or structure — exactly the kind Rebundler benefits the most.&lt;/p&gt;

&lt;div class=&quot;my-6 lg:-mx-24 xl:-mx-40 2xl:-mx-64&quot;&gt;
  &lt;div class=&quot;grid gap-4 md:grid-cols-2 [&amp;amp;_pre]:max-h-[40rem] [&amp;amp;_pre]:overflow-y-scroll [&amp;amp;_pre]:m-0&quot;&gt;
    &lt;div class=&quot;flex flex-col gap-2 min-w-0&quot;&gt;
      &lt;div class=&quot;text-center text-sm text-gray-400 mb-1&quot;&gt;Before&lt;/div&gt;
      &lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://rubygems.org&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;git_source&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:github&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://github.com/&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.git&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;ruby&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;file: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;.ruby-version&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Use Rails edge&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rails&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;github: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;rails/rails&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# The modern asset pipeline for Rails [https://github.com/rails/propshaft]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;propshaft&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Use sqlite3 as the database for Active Record&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;sqlite3&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;= 2.1.0&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Use the Puma web server [https://github.com/puma/puma]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;puma&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# use jbuilder for the api&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;jbuilder&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# gem &quot;jsbundling-rails&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Hotwire&apos;s SPA-like page accelerator [https://turbo.hotwired.dev]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;turbo-rails&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Hotwire&apos;s modest JavaScript framework [https://stimulus.hotwired.dev]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# gem &quot;stimulus-rails&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Bundle and process CSS [https://github.com/rails/cssbundling-rails]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# gem &quot;cssbundling-rails&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Use Redis adapter to run Action Cable in production&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# gem &quot;redis&quot;, &quot;&amp;gt;= 4.0.1&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# gem &quot;kredis&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;bcrypt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 3.1.7&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Windows does not include zoneinfo files, so bundle the tzinfo-data gem&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;tzinfo-data&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;platforms: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%i[windows jruby]&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Reduces boot times through caching; required in config/boot.rb&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;bootsnap&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Deploy this application anywhere as a Docker container [https://kamal-deploy.org]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;kamal&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;2.7.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;thruster&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;image_processing&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.2&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Image processing for event asset generation&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;mini_magick&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# All sorts of useful information about every country packaged as convenient little country objects&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;countries&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# ISO 639-1 and ISO 639-2 language code entries and convenience methods&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iso-639&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# A minimal client of Bluesky/ATProto API&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;minisky&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.4.0&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Extract Collaborator Objects from your Active Records, a new concept called Associated Objects&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;active_record-associated_object&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Headless Chrome driver for Capybara&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;cuprite&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# A single delightful Ruby way to work with AI.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby_llm&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.15.0&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# A simple and clean Ruby DSL for creating JSON schemas.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby_llm-schema&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;github: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;marcoroth/ruby_llm-schema&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;branch: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;conditionals&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# JSON Schema validator&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;json_schemer&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# YouTube V3 API client.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;yt&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Family of libraries that support various formats of XML &quot;feeds&quot;.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rss&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.3.1&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# YAML Editing and Refactoring with Better Accuracy&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;yerba&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.5.1&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Powerful and seamless HTML-aware ERB parsing and tooling.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;herb&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.9&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# An ActionView-compatible ERB engine with modern DX - re-imagined with Herb.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;reactionview&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.3&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Agnostic pagination in plain ruby.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;pagy&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# gem &quot;activerecord-enhancedsqlite3-adapter&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;solid_cache&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Database-backed Active Job backend.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;solid_queue&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;github: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;joshleblanc/solid_queue&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;branch: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;async-mode&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Operational controls for Active Job&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;mission_control-jobs&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Simple, powerful, first-party analytics for Rails&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ahoy_matey&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# The simplest way to group temporal data&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;groupdate&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Create beautiful JavaScript charts with one line of Ruby&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;chartkick&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 5.0&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Use Vite in Rails and bring joy to your JavaScript experience&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;vite_rails&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Collection of SEO helpers for Ruby on Rails.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;meta-tags&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Logs performance and exception data from your app to appsignal.com&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;appsignal&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Autoload dotenv in Rails.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;dotenv-rails&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Automatic generation of html links in texts&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rails_autolink&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.1&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Easily generate XML Sitemaps&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;sitemap_generator&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 6.3&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# A framework for building reusable, testable &amp;amp; encapsulated view components&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;view_component&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Adds ActiveRecord-specific methods to Dry::Initializer&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;dry-initializer-rails&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Type system for Ruby supporting coercions, constraints and complex types&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;dry-types&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.7&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Protocol Buffers are Google&apos;s data interchange format.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;google-protobuf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# ActiveJob::Performs adds the `performs` macro to set up jobs by convention.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;active_job-performs&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Use the OpenAI API with Ruby!&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby-openai&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Repairs broken JSON strings.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;json-repair&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.2.0&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Markdown that smells nice&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;redcarpet&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 3.6&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Syntax highlighting&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rouge&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 4.4&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Country Select Plugin&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;country_select&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Admin panel framework and Content Management System for Ruby on Rails.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;avo&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:avo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;optional: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;avo-pro&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;source: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://packager.dev/avo-hq/&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Marksmith is a GitHub-style markdown editor for Ruby on Rails applications.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;marksmith&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# A fast, safe, extensible parser for CommonMark. This wraps the comrak Rust crate.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;commonmarker&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;= 2.6.1&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# ActiveRecord like interface to read only access and query static YAML files&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;frozen_record&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.27.2&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# A convenient way to diff string in ruby&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;diffy&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# ActiveRecord soft-deletes done right&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;discard&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Makes consuming restful web services dead easy.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;httparty&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Use OmniAuth to support multi-provider authentication [https://github.com/omniauth/omniauth]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;omniauth&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Official OmniAuth strategy for GitHub.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;omniauth-github&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Provides a mitigation against CVE-2015-9284 [https://github.com/cookpad/omniauth-rails_csrf_protection]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;omniauth-rails_csrf_protection&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# An accessible autocomplete for Ruby on Rails apps using Hotwire.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;hotwire_combobox&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.4.0&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Common locale data and translations for Rails i18n.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rails-i18n&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 8.0&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Ruby standards gems&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;openssl&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# https://github.com/ruby/openssl/issues/949&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Class to build custom data structures, similar to a Hash.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ostruct&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Complete geocoding solution for Ruby.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;geocoder&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;github: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;alexreisner/geocoder&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Use latest geocoder with Nominatim improvements until next release&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# RubyGems.org API wrapper for gem information&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;gems&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# A glamorous CLI toolkit for Ruby&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;gum&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.3.1&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Regex pattern searching in files&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;grepfruit&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Create beautiful JavaScript maps with one line of Ruby&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;mapkick-rb&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.2.0&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Typesense Ruby client&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;typesense&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 4.1&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Typesense integration to your favorite ORM&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;typesense-rails&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.0.0.rc4&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:development&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:test&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;bundler-audit&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;debug&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;platforms: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%i[mri windows]&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;byebug&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;minitest-difftastic&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.2&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:development&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# A gem for generating annotations for Rails projects.&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;annotaterb&quot;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Use console on exceptions pages [https://github.com/rails/web-console]&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;web-console&quot;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rack-mini-profiler&quot;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# For call-stack profiling flamegraphs&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;stackprof&quot;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Speed up commands on slow machines / big apps [https://github.com/rails/spring]&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# gem &quot;spring&quot;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Use listen to watch files for changes [https://github.com/guard/listen]&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;listen&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 3.5&quot;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Guard for watching file changes and auto-importing [https://github.com/guard/guard]&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;guard&quot;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Benchmark is no longer included in the standard lib - used for benchmarking&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;benchmark&quot;&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby-lsp-rails&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;standardrb&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;erb_lint&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;authentication-zero&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:test&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;capybara&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rails-controller-testing&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;selenium-webdriver&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;vcr&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 6.1&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;webmock&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;icalendar&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 2.12&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;flex flex-col gap-2 min-w-0&quot;&gt;
      &lt;div class=&quot;text-center text-sm text-gray-400 mb-1&quot;&gt;After&lt;/div&gt;
      &lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://rubygems.org&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;git_source&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:github&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://github.com/&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.git&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;ruby&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;file: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;.ruby-version&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;active_job-performs&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# ActiveJob::Performs adds the `performs` macro to set up jobs by convention.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;active_record-associated_object&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Associate a Ruby PORO with an Active Record class and have it quack like one.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ahoy_matey&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Simple, powerful, first-party analytics for Rails&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;appsignal&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Logs performance and exception data from your app to appsignal.com&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;avo&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Admin panel framework and Content Management System for Ruby on Rails.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;bcrypt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 3.1.7&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# OpenBSD&apos;s bcrypt() password hashing algorithm.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;bootsnap&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Boot large ruby/rails apps faster&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;chartkick&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 5.0&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Create beautiful JavaScript charts with one line of Ruby&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;commonmarker&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;= 2.6.1&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# CommonMark parser and renderer. Written in Rust, wrapped in Ruby.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;countries&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Gives you a country object full of all sorts of useful information.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;country_select&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Country Select Plugin&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;cuprite&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Headless Chrome driver for Capybara&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;diffy&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A convenient way to diff string in ruby&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;discard&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# ActiveRecord soft-deletes done right&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;dotenv-rails&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Autoload dotenv in Rails.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;dry-initializer-rails&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Adds ActiveRecord-specific methods to Dry::Initializer&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;dry-types&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.7&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Type system for Ruby supporting coercions, constraints and complex types like structs, value objects, enums etc&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;frozen_record&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.27.2&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# ActiveRecord like interface to read only access and query static YAML files&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;gems&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Ruby wrapper for the RubyGems.org API&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;geocoder&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;github: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;alexreisner/geocoder&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Complete geocoding solution for Ruby.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;google-protobuf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Protocol Buffers&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;grepfruit&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Tool for searching regex patterns in files&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;groupdate&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# The simplest way to group temporal data&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;gum&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.3.1&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Ruby wrapper for Charm&apos;s gum CLI tool.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;herb&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.9&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# The modern HTML+ERB Toolchain&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;hotwire_combobox&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.4.0&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Accessible Autocomplete for Rails apps&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;httparty&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Makes http fun! Also, makes consuming restful web services dead easy.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;icalendar&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 2.12&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A ruby implementation of the iCalendar specification (RFC-5545).&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;image_processing&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.2&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# High-level wrapper for processing images for the web with ImageMagick or libvips.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;iso-639&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# ISO 639-1 and ISO 639-2 language code entries and convenience methods.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;jbuilder&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Create JSON structures via a Builder-style DSL&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;json-repair&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.2.0&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Repairs broken JSON strings.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;json_schemer&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# JSON Schema validator. Supports drafts 4, 6, 7, 2019-09, 2020-12, OpenAPI 3.0, and OpenAPI 3.1.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;kamal&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;2.7.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Deploy web apps in containers to servers running Docker with zero downtime.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;mapkick-rb&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.2.0&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Create beautiful JavaScript maps with one line of Ruby&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;marksmith&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Marksmith is a GitHub-style markdown editor for Ruby on Rails applications.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;meta-tags&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Collection of metadata helpers for Ruby on Rails.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;mini_magick&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Manipulate images with minimal use of memory via ImageMagick&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;minisky&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.4.0&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A minimal client of Bluesky/ATProto API&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;mission_control-jobs&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Operational controls for Active Job&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;omniauth&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A generalized Rack framework for multiple-provider authentication.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;omniauth-github&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Official OmniAuth strategy for GitHub.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;omniauth-rails_csrf_protection&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Provides CSRF protection on OmniAuth request endpoint on Rails application.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;openssl&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# SSL/TLS and general-purpose cryptography for Ruby&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ostruct&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Class to build custom data structures, similar to a Hash.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;pagy&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Pagy 🐸 The Leaping Gem!&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;propshaft&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Deliver assets for Rails.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;puma&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A Ruby/Rack web server built for parallelism.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rails&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;github: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;rails/rails&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Full-stack web application framework.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rails_autolink&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.1&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Automatic generation of html links in texts&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rails-i18n&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 8.0&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Common locale data and translations for Rails i18n.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;reactionview&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.3&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# An ActionView-compatible ERB engine with modern DX - re-imagined with Herb.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;redcarpet&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 3.6&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Markdown that smells nice&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rouge&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 4.4&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A pure-ruby colorizer based on pygments&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rss&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.3.1&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Family of libraries that support various formats of XML &quot;feeds&quot;.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby_llm&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.15.0&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# One beautiful Ruby API for GPT, Claude, Gemini, and more.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby_llm-schema&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;github: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;marcoroth/ruby_llm-schema&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;branch: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;conditionals&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A simple Ruby DSL for creating JSON schemas.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby-openai&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# OpenAI API + Ruby! 🤖❤️&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;sitemap_generator&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 6.3&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Easily generate XML Sitemaps&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;solid_cache&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A database backed ActiveSupport::Cache::Store&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;solid_queue&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;github: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;joshleblanc/solid_queue&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;branch: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;async-mode&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Database-backed Active Job backend.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;sqlite3&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;= 2.1.0&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Ruby library to interface with the SQLite3 database engine (http://www.sqlite.org).&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;thruster&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Zero-config HTTP/2 proxy&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;turbo-rails&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# The speed of a single-page web application without having to write any JavaScript.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;typesense&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 4.1&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Ruby Library for Typesense&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;typesense-rails&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.0.0.rc4&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;tzinfo-data&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;platforms: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%i[windows jruby]&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Timezone Data for TZInfo&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;view_component&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A framework for building reusable, testable &amp;amp; encapsulated view components in Ruby on Rails.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;vite_rails&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Use Vite in Rails and bring joy to your JavaScript experience&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;yerba&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.5.1&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# YAML Editing and Refactoring with Better Accuracy&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;yt&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Yt makes it easy to interact with Youtube V3 API by providing a modular, intuitive and tested Ruby-style API.&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:avo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;optional: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;avo-pro&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;source: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://packager.dev/avo-hq/&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Visit https://avohq.io/ to get more information about this gem.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:development&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;annotaterb&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A gem for generating annotations for Rails projects.&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;authentication-zero&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# An authentication system generator for Rails applications&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;benchmark&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# a performance benchmarking library&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;erb_lint&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# ERB lint tool&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;guard&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Guard keeps an eye on your file modifications&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;listen&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 3.5&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Listen to file modifications&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rack-mini-profiler&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Profiles loading speed for rack applications.&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby-lsp-rails&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A Ruby LSP addon for Rails&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;stackprof&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# sampling callstack-profiler for ruby 2.2+&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;standardrb&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Alias for the standard gem, which has a standardrb binary&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;web-console&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# A debugging tool for your Ruby on Rails applications.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:development&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:test&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;bundler-audit&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;require: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Patch-level verification for Bundler&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;byebug&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Ruby fast debugger - base + CLI&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;debug&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;platforms: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%i[mri windows]&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Debugging functionality for Ruby&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;minitest-difftastic&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 0.2&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Minitest Plugin to use difftastic for failed assertions&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:test&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;capybara&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Capybara aims to simplify the process of integration testing Rack applications, such as Rails, Sinatra or Merb&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rails-controller-testing&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Extracting `assigns` and `assert_template` from ActionDispatch.&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;selenium-webdriver&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Selenium is a browser automation tool for automated testing of webapps and more&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;vcr&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 6.1&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Record your test suite&apos;s HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests.&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;webmock&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Library for stubbing HTTP requests in Ruby.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Give it a spin on your own Gemfile in the &lt;a href=&quot;https://rebundler.dev/&quot;&gt;Rebundler Playground&lt;/a&gt;, or &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle add rebundler&lt;/code&gt; and run it locally. I’d love to hear what you think!&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;It seems that &lt;a href=&quot;https://x.com/kddnewton/status/2050982990558179693&quot;&gt;might be coming&lt;/a&gt; via syntax_tree soon, though. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
      &lt;p&gt;The &lt;a href=&quot;https://x.com/kddnewton/status/2050982997331972400&quot;&gt;next version of Prism&lt;/a&gt; will make that a lot easier. &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;</content><author><name>Dennis Paagman</name><email>dennis@paagman.dev</email></author><category term="ruby" /></entry><entry><title type="html">Making sure local CI is green before deploying to Heroku</title><link href="https://paagman.dev/rails-local-ci-with-heroku/" rel="alternate" type="text/html" title="Making sure local CI is green before deploying to Heroku" /><published>2025-11-18T00:00:00+01:00</published><updated>2025-11-18T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2025-11-18-local-ci-with-heroku.md</id><content type="html" xml:base="https://paagman.dev/rails-local-ci-with-heroku/">&lt;p&gt;&lt;em&gt;Note: this article assumes you’re using GitHub, something similar can probably also be cooked up for other hosting providers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://rubyonrails.org/2025/10/22/rails-8-1&quot;&gt;Rails 8.1 was released recently&lt;/a&gt; with a new ‘Local CI’ feature. This allows you to easily define and run the steps you traditionally run on an external system (like GitHub Actions) on your own machine. Modern hardware is fast enough to even run large test suites nowadays.&lt;/p&gt;

&lt;p&gt;One benefit of using an external system is that they can run your CI steps structurally and independently and you can prevent PRs from being merged if the linters and test suites are failing, adding famous green checks to your pull requests.&lt;/p&gt;

&lt;p&gt;Local CI also does this by including a “signoff” step which basically signals to GitHub that your commit has successfully run the CI suite locally (Works On My Machine™). This is great and allows you to keep some certainty of the state of your code and your pull requests.&lt;/p&gt;

&lt;p&gt;With local CI there is a missing piece though: once a PR gets merged there is no final check for the merge commit on your main branch. All external tools I know run another build for the merge commit and I usually wait for those before deploying (or automated deploy processes usually do).&lt;/p&gt;

&lt;p&gt;Since I deploy directly to Heroku through the command line (&lt;code class=&quot;highlighter-rouge&quot;&gt;git push heroku main&lt;/code&gt; 🤘) there is no such check anymore and I have to remember to run and check the command myself.&lt;/p&gt;

&lt;p&gt;To prevent myself from forgetting I’ve written a small &lt;a href=&quot;https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks&quot;&gt;git pre push hook&lt;/a&gt; that runs the local CI suite if necessary and holds the push if it fails. This serves as a nice backstop while keeping all the benefits from running everything locally.&lt;/p&gt;

&lt;p&gt;What it basically does:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Check if the current commit has already been signed off.&lt;/li&gt;
  &lt;li&gt;If not: run the CI suite (which then marks the commit as signed off).&lt;/li&gt;
  &lt;li&gt;Blocks the push if it fails, else continue and deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives me peace of mind that no bugs will make it to production (&lt;em&gt;right 🥹&lt;/em&gt;).&lt;/p&gt;

&lt;h1 id=&quot;setup&quot;&gt;Setup&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;Install the &lt;code class=&quot;highlighter-rouge&quot;&gt;gh&lt;/code&gt; cli tool and authenticate with GitHub (&lt;code class=&quot;highlighter-rouge&quot;&gt;gh auth login&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;Create a new file in your app’s folder: &lt;code class=&quot;highlighter-rouge&quot;&gt;.git/hooks/pre-push&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-euo&lt;/span&gt; pipefail

&lt;span class=&quot;c&quot;&gt;# Pre-push hook that only runs on main branch when pushing to Heroku&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;remote&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;IFS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos; &apos;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; local_ref local_sha remote_ref remote_sha&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# Only check main branch pushes to Heroku&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$remote_ref&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;refs/heads/main&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$remote&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;heroku&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    continue
  fi

  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Pushing to main branch on Heroku...&quot;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Checking if commit &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$local_sha&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; has passed status checks on GitHub...&quot;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Get commit status from GitHub&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;status_response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;gh api &lt;span class=&quot;s2&quot;&gt;&quot;repos/:owner/:repo/commits/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$local_sha&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/status&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$status_response&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | jq &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;.state // &quot;unknown&quot;&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$status&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in
  &lt;/span&gt;success&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;✅ Commit has passed GitHub status checks, skipping bin/ci&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
  pending&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;⏳ GitHub checks are still pending. Running local CI...&quot;&lt;/span&gt;
    bin/ci &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;❌ bin/ci failed! Push aborted.&quot;&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;✅ bin/ci passed!&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
  failure &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; error&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;❌ GitHub checks failed with status: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$status&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Running local CI to verify...&quot;&lt;/span&gt;
    bin/ci &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;❌ bin/ci failed! Push aborted.&quot;&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;✅ bin/ci passed!&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;⚠️ Unknown GitHub status: &apos;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$status&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&apos;. Running local CI...&quot;&lt;/span&gt;
    bin/ci &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;❌ bin/ci failed! Push aborted.&quot;&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;✅ bin/ci passed!&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;esac&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And you’re done ✅! Only green commits will be deployed to Heroku.&lt;/p&gt;</content><author><name>Dennis Paagman</name><email>dennis@paagman.dev</email></author><category term="ruby-on-rails" /></entry><entry><title type="html">Hotwire Native is extremely future proof</title><link href="https://paagman.dev/hotwire-native-is-extremely-future-proof/" rel="alternate" type="text/html" title="Hotwire Native is extremely future proof" /><published>2025-09-16T00:00:00+02:00</published><updated>2025-09-16T00:00:00+02:00</updated><id>repo://posts.collection/_posts/2025-09-16-hotwire-native-is-extremely-future-proof.md</id><content type="html" xml:base="https://paagman.dev/hotwire-native-is-extremely-future-proof/">&lt;p&gt;Yesterday, &lt;a href=&quot;https://www.apple.com/newsroom/2025/09/new-versions-of-apples-software-platforms-are-available-today/&quot;&gt;iOS 26 was released&lt;/a&gt; with a whole new look-and-feel called ‘liquid glass’.&lt;/p&gt;

&lt;p&gt;You might think that getting that to work with your Hotwire Native app requires at least writing &lt;em&gt;some&lt;/em&gt; code. But because of the tight integration with iOS (using first party, native UI components) it actually does not need any code changes at all.&lt;/p&gt;

&lt;p&gt;Upgrading your app to the new look requires exactly &lt;strong&gt;0 lines of code&lt;/strong&gt;! Just a few clicks and some patience:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Update Xcode.&lt;/li&gt;
  &lt;li&gt;Rebuild your app against iOS 26.&lt;/li&gt;
  &lt;li&gt;✨ literally nothing else ✨.&lt;/li&gt;
  &lt;li&gt;Push to the App Store.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And you’re ready to go. Your users will like that your app matches the new shiny looks of their OS, including all new animations and effects.&lt;/p&gt;

&lt;div class=&quot;flex flex-col md:flex-row gap-3 px-6&quot;&gt;
  &lt;figure class=&quot;md:w-1/2 flex flex-col items-center&quot;&gt;
    &lt;img src=&quot;/images/posts/RocketSim_Screenshot_iPhone_16e_6.1_2025-09-16_17.39.57.jpeg&quot; /&gt;
    &lt;figcaption&gt;iOS 18&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;figure class=&quot;md:w-1/2 flex flex-col items-center&quot;&gt;
    &lt;img src=&quot;/images/posts/RocketSim_Screenshot_iPhone_16e_6.1_2025-09-16_17.38.31.jpeg&quot; /&gt;
    &lt;figcaption&gt;iOS 26 (w/ liquid glass)&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;/div&gt;

&lt;p&gt;Joe Masilotti also touched on this in his &lt;a href=&quot;https://youtu.be/VbMt_4STWIo?list=PLHFP2OPUpCebhAv1ZWb_978cTl1o-yue-&quot;&gt;Rails World 2025 keynote&lt;/a&gt; (check out it if you haven’t, it’s awesome).&lt;/p&gt;

&lt;p&gt;My main take away is something else though: &lt;strong&gt;things like this makes Hotwire Native extremely future proof.&lt;/strong&gt; Any changes to the underlying platforms will almost automatically just work in your app. You just need to rebuild and push a new version every now and then.&lt;/p&gt;

&lt;p&gt;This makes building on top of Hotwire and Hotwire Native the best way to accompany your Rails app with native apps. You can have the best of both worlds and focus on building your web app the way you’re used to, adding some sprinkles for native behavior when needed.&lt;/p&gt;

&lt;p&gt;Changes in your web app will automatically be available to your app users when you deploy and platform changes will automatically trickle down to your apps as well. It only require a new build every now and then.&lt;/p&gt;

&lt;p&gt;This made my believe in the future of Hotwire Native and Rails even more solid. Exciting times are ahead!&lt;/p&gt;</content><author><name>Dennis Paagman</name><email>dennis@paagman.dev</email></author><category term="hotwire-native" /></entry><entry><title type="html">Handling chrome.devtools.json requests in Rails apps</title><link href="https://paagman.dev/handling-chrome-devtools-json-requests-in-rails-apps/" rel="alternate" type="text/html" title="Handling chrome.devtools.json requests in Rails apps" /><published>2025-08-28T00:00:00+02:00</published><updated>2025-08-28T00:00:00+02:00</updated><id>repo://posts.collection/_posts/2025-08-28-chrome-devtools-middleware.md</id><content type="html" xml:base="https://paagman.dev/handling-chrome-devtools-json-requests-in-rails-apps/">&lt;p&gt;Chrome recently added a new feature called “&lt;a href=&quot;https://chromium.googlesource.com/devtools/devtools-frontend/+/main/docs/ecosystem/automatic_workspace_folders.md&quot;&gt;Automatic Workspace Folders&lt;/a&gt;” to Chrome DevTools. It can help with automatically mapping the correct folder of a project to the Workspace feature of the web inspector. In the workspace you can, for example, directly edit and save files in your project.&lt;/p&gt;

&lt;p&gt;I don’t use the feature, but errors from requests to a specific file for this feature in my logs were cluttering my log output. Basically Chrome is constantly trying to fetch a JSON file:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Started GET “/.well-known/appspecific/com.chrome.devtools.json” for ::1 at ….&lt;/p&gt;

  &lt;p&gt;ActionController::RoutingError (No route matches [GET] “/.well-known/appspecific/com.chrome.devtools.json”):&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To resolve this I wrote a small Rack middleware class that handles these requests and responds in the way Chrome wants. That solves the problem of the cluttered log entries and you can automatically set up the Workspace feature if you want.
It needs a small JSON response with the path to the project and a unique UUID.&lt;/p&gt;

&lt;p&gt;There are some other solutions out there, like a &lt;a href=&quot;https://github.com/ChromeDevTools/vite-plugin-devtools-json&quot;&gt;Vite plugin&lt;/a&gt; or a small Ruby gem (&lt;a href=&quot;https://github.com/6temes/chrome_devtools_rails&quot;&gt;chrome_devtools_rails&lt;/a&gt;), but the nice thing about this small middleware is that you can simply add it to your codebase without adding an extra dependency.&lt;/p&gt;

&lt;p&gt;Simply create the following file as &lt;code class=&quot;highlighter-rouge&quot;&gt;lib/middleware/chrome_devtools.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;digest&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;json&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Middleware&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Chrome DevTools middleware that intercepts requests to the Chrome DevTools&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# `com.chrome.devtools.json` endpoint and returns a workspace configuration.&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# This allows Chrome DevTools to automatically detect and connect to the project&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# workspace in their developer tools.&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# For more info: See https://chromium.googlesource.com/devtools/devtools-frontend/+/main/docs/ecosystem/automatic_workspace_folders.md&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ChromeDevtools&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;NAMESPACE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;822f7bc5-aa31-4b9f-9c14-df23d95578a1&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# randomly generated when writing this code, you can change it to any UUID you want.&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;PATH&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/.well-known/appspecific/com.chrome.devtools.json&quot;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;vi&quot;&gt;@app&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PATH_INFO&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;PATH&lt;/span&gt;

      &lt;span class=&quot;n&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;workspace: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;ss&quot;&gt;uuid: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;generate_uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;ss&quot;&gt;root: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;Cache-Control&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;public, max-age=31536000, immutable&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;Expires&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;httpdate&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;kp&quot;&gt;private&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;generate_uuid&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Digest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;uuid_v5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;NAMESPACE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And enable the middleware in your &lt;code class=&quot;highlighter-rouge&quot;&gt;config/environments/development.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;./lib/middleware/chrome_devtools&quot;&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;configure&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Middleware&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ChromeDevtools&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it!&lt;/p&gt;</content><author><name>Dennis Paagman</name><email>dennis@paagman.dev</email></author><category term="ruby-on-rails" /></entry><entry><title type="html">Deep link into apps with Hotwire Native and universal links (iOS)</title><link href="https://paagman.dev/deep-link-hotwire-native-universal-links/" rel="alternate" type="text/html" title="Deep link into apps with Hotwire Native and universal links (iOS)" /><published>2025-02-27T00:00:00+01:00</published><updated>2025-02-27T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2025-02-27-deep-link-hotwire-native-universal-links.md</id><content type="html" xml:base="https://paagman.dev/deep-link-hotwire-native-universal-links/">&lt;div class=&quot;p-5 bg-yellow-200&quot;&gt;
  This post was updated in October 2025 to work with the latest version of Hotwire Native (1.2 and up).
&lt;/div&gt;

&lt;p&gt;By default all external links in your Hotwire Native app will open in a Safari modal. This is great for most cases, but it’s also very nice to directly open links in a specific app, for example, when linking to a location on Google Maps, or an Instagram post.&lt;/p&gt;

&lt;p&gt;This is where &lt;a href=&quot;https://developer.apple.com/documentation/xcode/allowing-apps-and-websites-to-link-to-your-content/&quot;&gt;universal links&lt;/a&gt; come in. They are just standard https links that you can also open in the browser, but if the user has the app installed, the system will open the link directly in the app.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;When users tap or click a universal link, the system redirects the link directly to your app without routing through Safari or your website. In addition, because universal links are standard HTTP or HTTPS links, one URL works for both your website and your app. If the user has not installed your app, the system opens the URL in Safari, allowing your website to handle it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;hotwire-native&quot;&gt;Hotwire Native&lt;/h3&gt;

&lt;p&gt;Fortunately, Hotwire Native &lt;a href=&quot;https://github.com/hotwired/hotwire-native-ios/pull/105&quot;&gt;offers some flexibility&lt;/a&gt; on how to deal with links through ‘route decision handlers’ (introduced in 1.2.0), and with a little bit of code we can open links that have a the corresponding app installed in their app, and open the rest in a Safari modal.&lt;/p&gt;

&lt;p&gt;There are two built-in handlers:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Open links through the system (&lt;code class=&quot;highlighter-rouge&quot;&gt;SystemNavigationRouteDecisionHandler&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;Open links in a Safari modal (&lt;code class=&quot;highlighter-rouge&quot;&gt;SafariViewControllerRouteDecisionHandler&lt;/code&gt;), the default.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first option works well with universal links, they open in their app as expected. But if you &lt;strong&gt;don’t&lt;/strong&gt; have the app installed (or there is no app at all), those links will now be opened in the Safari app, no longer in a modal, and the user will have left your app.&lt;/p&gt;

&lt;p&gt;Instead of using one of the options above, we can also write our own custom handler and have the best of both worlds. We’ll call it the &lt;code class=&quot;highlighter-rouge&quot;&gt;UniversalRouteDecisionHandler&lt;/code&gt;. Put the following in a file named &lt;code class=&quot;highlighter-rouge&quot;&gt;UniversalRouteDecisionHandler.swift&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It’s based on the &lt;code class=&quot;highlighter-rouge&quot;&gt;SafariViewControllerRouteDecisionHandler&lt;/code&gt;, especially the code for &lt;code class=&quot;highlighter-rouge&quot;&gt;matches&lt;/code&gt; is copied as it is. The ‘magic’ happens in the &lt;code class=&quot;highlighter-rouge&quot;&gt;handle&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;Unfortunately, as of today there is no other way to detect if an app answers to an universal links then trying to open it and see what happens. We tell the system to open the link (but as an universal link only), and if it fails, we fall back by calling Hotwire Native’s built in &lt;code class=&quot;highlighter-rouge&quot;&gt;SafariViewControllerRouteDecisionHandler&lt;/code&gt; to handle the link.&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Foundation&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;HotwireNative&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIKit&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UniversalRouteDecisionHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;RouteDecisionHandler&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;universal-route-decision-handler&quot;&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;matches&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;nv&quot;&gt;configuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Navigator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Configuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Bool&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;/// SFSafariViewController will crash if we pass along a URL that&apos;s not valid.&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;guard&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scheme&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;http&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scheme&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;https&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;#available&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iOS&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;configuration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startLocation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;configuration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startLocation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;host&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;host&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                       &lt;span class=&quot;nv&quot;&gt;configuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Navigator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Configuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                       &lt;span class=&quot;nv&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Router&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Decision&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;UIApplication&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shared&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;universalLinksOnly&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;success&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;success&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;c1&quot;&gt;// no app to handle the link, open in a modal through the built-in SafariViewControllerRouteDecisionHandler&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;handler&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SafariViewControllerRouteDecisionHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

                &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;handler&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                   &lt;span class=&quot;nv&quot;&gt;configuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;configuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                   &lt;span class=&quot;nv&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cancel&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The only thing left is to register our new handler in your &lt;code class=&quot;highlighter-rouge&quot;&gt;AppDelegate&lt;/code&gt;. We keep some of the existing stack of handlers as well, as they handle internal links (&lt;code class=&quot;highlighter-rouge&quot;&gt;AppNavigationRouteDecisionHandler&lt;/code&gt;) and other links, like &lt;code class=&quot;highlighter-rouge&quot;&gt;email:...&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;tel:...&lt;/code&gt; or other custom URL schemes (&lt;code class=&quot;highlighter-rouge&quot;&gt;SystemNavigationRouteDecisionHandler&lt;/code&gt;).&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;Hotwire&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;registerRouteDecisionHandlers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;AppNavigationRouteDecisionHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;UniversalRouteDecisionHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// this replaces `SafariViewControllerRouteDecisionHandler()`&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;SystemNavigationRouteDecisionHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This way your users will have the best user experience for all links in your app. If a link has a corresponding app installed, that will be used. If not, the link will open in a modal Safari screen in your app.&lt;/p&gt;</content><author><name>Dennis Paagman</name><email>dennis@paagman.dev</email></author><category term="hotwire-native" /></entry><entry><title type="html">Make delete actions stand out in Hotwire Native menus</title><link href="https://paagman.dev/make-delete-actions-stand-out-in-hotwire-native-menus/" rel="alternate" type="text/html" title="Make delete actions stand out in Hotwire Native menus" /><published>2025-01-06T00:00:00+01:00</published><updated>2025-01-06T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2025-01-06-destructive.md</id><content type="html" xml:base="https://paagman.dev/make-delete-actions-stand-out-in-hotwire-native-menus/">&lt;p&gt;The Hotwire Native menu bridge is one of the most common ways to interact with native functionality from your Hotwire Native app. It is part of the demo application and probably one of the first components you will implement when you start building your own Hotwire Native app. It instantly makes your app feel more native.&lt;/p&gt;

&lt;p&gt;A common design pattern is to show ‘destructive’ actions (deleting a record, for example) in a different color (usually red) to indicate that the action is potentially dangerous and make them stand out from the other actions.&lt;/p&gt;

&lt;p&gt;The example menu component has no way to do this out of the box, so in this article I will show you how to add different styling for such actions with just a few lines of code!&lt;/p&gt;

&lt;div class=&quot;flex flex-col md:flex-row gap-3 px-6&quot;&gt;
  &lt;figure class=&quot;md:w-1/2 flex flex-col items-center&quot;&gt;
    &lt;img src=&quot;/images/posts/hotwire_native_menu.png&quot; /&gt;
    &lt;figcaption&gt;The current situation&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;figure class=&quot;md:w-1/2 flex flex-col items-center&quot;&gt;
    &lt;img src=&quot;/images/posts/hotwire_native_menu_destructive.png&quot; /&gt;
    &lt;figcaption&gt;What we&apos;ll end up with&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;/div&gt;

&lt;h2 id=&quot;the-bridge-component-ios&quot;&gt;The Bridge component (iOS)&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;All code is based on the example app’s &lt;a href=&quot;https://github.com/hotwired/hotwire-native-ios/blob/1.1.0/Demo/Bridge/MenuComponent.swift&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;MenuComponent&lt;/code&gt;&lt;/a&gt; and the corresponding Stimulus controller from the &lt;a href=&quot;https://github.com/hotwired/hotwire-native-demo/blob/f5d60c343a1cf741f50aaec930e2ff17c267df26/public/javascript/controllers/bridge/menu_controller.js&quot;&gt;Hotwire Native demo app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The menu is constructed from &lt;a href=&quot;https://developer.apple.com/documentation/uikit/uialertaction&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;UIAlertAction&lt;/code&gt;&lt;/a&gt; classes in the iOS app’s &lt;code class=&quot;highlighter-rouge&quot;&gt;MenuComponent&lt;/code&gt;. They allow for little configuration, but do have a &lt;code class=&quot;highlighter-rouge&quot;&gt;style&lt;/code&gt; property that can be set to &lt;code class=&quot;highlighter-rouge&quot;&gt;.destructive&lt;/code&gt; to indicate a destructive action, which makes the text of the item red. We’ll use this property to style our menu item differently.&lt;/p&gt;

&lt;p&gt;The current implementation always uses the &lt;code class=&quot;highlighter-rouge&quot;&gt;.default&lt;/code&gt; style, so we’re going to keep that as, well, the default.&lt;/p&gt;

&lt;p&gt;To distinguish between regular and destructive menu items, we add a &lt;code class=&quot;highlighter-rouge&quot;&gt;destructive&lt;/code&gt; boolean property to the &lt;code class=&quot;highlighter-rouge&quot;&gt;Item&lt;/code&gt; struct (which is part of the data structure received from the bridge component), and set the &lt;code class=&quot;highlighter-rouge&quot;&gt;style&lt;/code&gt; property of the &lt;code class=&quot;highlighter-rouge&quot;&gt;UIAlertAction&lt;/code&gt; based on it’s value.&lt;/p&gt;

&lt;p&gt;That leads to the following changes:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;// MenuComponent.swift
// source: https://github.com/hotwired/hotwire-native-ios/blob/1.1.0/Demo/Bridge/MenuComponent.swift
&lt;span class=&quot;err&quot;&gt;
&lt;/span&gt;// lines 39-43
&lt;span class=&quot;p&quot;&gt;for item in items {
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;-   let action = UIAlertAction(title: item.title, style: .default) { [unowned self] _ in
-       onItemSelected(item: item)
-   }
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+   let action = UIAlertAction(title: item.title,
+                              style: item.destructive ? .destructive : .default)
+   { [unowned self] _ in
+       onItemSelected(item: item)
+   }
&lt;/span&gt;    alertController.addAction(action)
}
&lt;span class=&quot;err&quot;&gt;
&lt;/span&gt;// ...
&lt;span class=&quot;err&quot;&gt;
&lt;/span&gt;# line 89
&lt;span class=&quot;p&quot;&gt;struct Item: Decodable {
&lt;/span&gt;    let title: String
    let index: Int
&lt;span class=&quot;gi&quot;&gt;+   let destructive: Bool
&lt;/span&gt;}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The formatting is a bit different, but the main functional change is the addition of the ternary operator that sets &lt;code class=&quot;highlighter-rouge&quot;&gt;style&lt;/code&gt; based on the new property.&lt;/p&gt;

&lt;h2 id=&quot;the-bridge-component-android&quot;&gt;The Bridge component (Android)&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Thanks to &lt;a href=&quot;https://x.com/_leonvogt&quot;&gt;Leon Vogt&lt;/a&gt; for providing tips for the Android implementation!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Android implementation is a bit different, but the concept is the same. We need to add a &lt;code class=&quot;highlighter-rouge&quot;&gt;destructive&lt;/code&gt; property to the &lt;code class=&quot;highlighter-rouge&quot;&gt;Item&lt;/code&gt; data class and use it to set the text color of the menu item. Android itself has no notion of ‘destructive’ actions, so we set to set the color of the text ourselves.&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;// MenuComponent.kt
// source: https://github.com/hotwired/hotwire-native-android/blob/1.1.1/demo/src/main/kotlin/dev/hotwire/demo/bridge/MenuComponent.kt
&lt;span class=&quot;err&quot;&gt;
&lt;/span&gt;// line 72
@Serializable
&lt;span class=&quot;p&quot;&gt;data class Item(
&lt;/span&gt;    @SerialName(&quot;title&quot;) val title: String,
    @SerialName(&quot;index&quot;) val index: Int
&lt;span class=&quot;gi&quot;&gt;+   @SerialName(&quot;destructive&quot;) val destructive: Boolean
&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;// MenuComponentAdapter.kt
// source: https://github.com/hotwired/hotwire-native-android/blob/1.1..1/demo/src/main/kotlin/dev/hotwire/demo/bridge/MenuComponentAdapter.kt
&lt;span class=&quot;err&quot;&gt;
&lt;/span&gt;// line 50
&lt;span class=&quot;p&quot;&gt;fun bind(item: MenuComponent.Item) {
&lt;/span&gt;    textView.text = item.title
&lt;span class=&quot;err&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+   if (item.destructive) {
+       textView.setTextColor(Color.RED)
+   } else {
+       textView.setTextColor(Color.BLACK)
+   }
+
&lt;/span&gt;    itemView.setOnClickListener {
        action?.invoke(item)
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;the-stimulus-bridge-controller&quot;&gt;The Stimulus bridge controller&lt;/h2&gt;

&lt;p&gt;We now need to change the payload that is sent from the stimulus controller. The &lt;code class=&quot;highlighter-rouge&quot;&gt;menuItem&lt;/code&gt; function inside the controller constructs the data for each menu item, so that’s where we need to add our new &lt;code class=&quot;highlighter-rouge&quot;&gt;destructive&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;We could use a custom Stimulus value or data attribute and set it explicitly for the items we want to use it for. But since we already use Turbo there’s a good chance there’s already a &lt;code class=&quot;highlighter-rouge&quot;&gt;data-turbo-method&lt;/code&gt; attribute set on link that do deletes, as Turbo uses that to send requests with the right HTTP request method.&lt;/p&gt;

&lt;p&gt;We can leverage that attribute to set the &lt;code class=&quot;highlighter-rouge&quot;&gt;destructive&lt;/code&gt; property directly. This means that all menu items that link to a &lt;code class=&quot;highlighter-rouge&quot;&gt;DELETE&lt;/code&gt; action will automatically be marked as destructive, without changing any of the underlying HTML.&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;// app/javascript/controllers/bridge/menu_controller.js
// source: https://github.com/hotwired/hotwire-native-demo/blob/f5d60c343a1cf741f50aaec930e2ff17c267df26/public/javascript/controllers/bridge/menu_controller.js
&lt;span class=&quot;err&quot;&gt;
&lt;/span&gt;// line 34
&lt;span class=&quot;p&quot;&gt;menuItem(element, index) {
&lt;/span&gt;  const bridgeElement = new BridgeElement(element)
&lt;span class=&quot;err&quot;&gt;
&lt;/span&gt;  if (bridgeElement.disabled) return null
&lt;span class=&quot;err&quot;&gt;
&lt;/span&gt;  return {
    title: bridgeElement.title,
    index: index,
&lt;span class=&quot;gi&quot;&gt;+   destructive: element.dataset.turboMethod === &quot;delete&quot;,
&lt;/span&gt;  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it! All menu items that link to a &lt;code class=&quot;highlighter-rouge&quot;&gt;DELETE&lt;/code&gt; action will now be marked as destructive and shown in red in the native menu.&lt;/p&gt;</content><author><name>Dennis Paagman</name><email>dennis@paagman.dev</email></author><category term="hotwire-native" /></entry><entry><title type="html">Building a Native sharing component with Hotwire Native (iOS)</title><link href="https://paagman.dev/hotwire-native-share-component/" rel="alternate" type="text/html" title="Building a Native sharing component with Hotwire Native (iOS)" /><published>2024-12-11T00:00:00+01:00</published><updated>2024-12-11T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2024-12-11-hotwire-native-share.md</id><content type="html" xml:base="https://paagman.dev/hotwire-native-share-component/">&lt;video controls=&quot;&quot; loop=&quot;&quot; autoplay=&quot;&quot; class=&quot;md:float-right md:ml-4 md:w-1/3&quot;&gt;
  &lt;source src=&quot;/videos/hotwire-native-bridge-share.mp4&quot; /&gt;
&lt;/video&gt;

&lt;p&gt;One of the great things about building with Hotwire Native is being able to easily tap into native functionality. A common use case in mobile apps is the ability to share content using the native share functionality. It’s the most center button in Safari, after all. Let’s look at how we can implement this using Hotwire Native’s bridge components!
The implementation consists of three parts:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;A Stimulus bridge component that handles the web interface.&lt;/li&gt;
  &lt;li&gt;A Swift bridge component that opens the native sharing modal.&lt;/li&gt;
  &lt;li&gt;HTML markup to wire everything up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s break down each piece:&lt;/p&gt;

&lt;h2 id=&quot;the-stimulus-bridge-controller&quot;&gt;The Stimulus bridge controller&lt;/h2&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/javascript/controllers/bridge/share_controller.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;BridgeComponent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@hotwired/hotwire-native-bridge&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;BridgeComponent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;component&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;share&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;href&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This simple Stimulus controller doesn’t need to do much. It defines the bridge component’s name and an &lt;code class=&quot;highlighter-rouge&quot;&gt;open&lt;/code&gt; function, which sends a message to the native layer with the current page URL as it’s data payload. Since we only need to send some data one way (to the bridge layer) when the open action is performed, we do not need to override the &lt;code class=&quot;highlighter-rouge&quot;&gt;connect()&lt;/code&gt; function or define a callback function.&lt;/p&gt;

&lt;h2 id=&quot;the-bridge-component&quot;&gt;The Bridge component&lt;/h2&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// ShareComponent.swift&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;HotwireNative&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIKit&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ShareComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;BridgeComponent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;share&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;onReceive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;guard&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;MessageData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;nf&quot;&gt;openShareModal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;viewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIViewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;destination&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as?&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIViewController&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;openShareModal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;activityViewController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIActivityViewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;activityItems&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;applicationActivities&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;viewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;present&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;activityViewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;animated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ShareComponent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;MessageData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Decodable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The Swift component handles the native part of the implementation. When it receives a message from the &lt;code class=&quot;highlighter-rouge&quot;&gt;share&lt;/code&gt; component, it creates a &lt;a href=&quot;https://developer.apple.com/documentation/uikit/uiactivityviewcontroller&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;UIActivityViewController&lt;/code&gt;&lt;/a&gt; - iOS’s native sharing interface - with the url from the data payload, and opens it by passing it to the &lt;code class=&quot;highlighter-rouge&quot;&gt;viewController&lt;/code&gt;. We only have one possible event (&lt;code class=&quot;highlighter-rouge&quot;&gt;open&lt;/code&gt;), so there is no need to check for specific message types in the &lt;code class=&quot;highlighter-rouge&quot;&gt;onReceive&lt;/code&gt; function and we can just assume it will always be that event.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;There are some additional options to pass to the &lt;code class=&quot;highlighter-rouge&quot;&gt;UIActivityViewController&lt;/code&gt; initializer, you can for example exclude certain activity types or sections. Check out the documentation for more information.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;tying-it-all-together-the-html-part&quot;&gt;Tying it all together: the HTML part&lt;/h2&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bridge--share&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bridge--share#open&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Share&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Implementing the HTML markup is straightforward. We add a button that triggers the &lt;code class=&quot;highlighter-rouge&quot;&gt;open&lt;/code&gt; action on the &lt;code class=&quot;highlighter-rouge&quot;&gt;bridge--share&lt;/code&gt; controller when clicked.&lt;/p&gt;

&lt;p&gt;You can also combine this with other Hotwire Native components, for example inside a &lt;a href=&quot;https://hotwire-native-demo.dev/bridge-menu&quot;&gt;menu&lt;/a&gt;, because those will simply trigger a click event, which in turn triggers the action of our share component.&lt;/p&gt;

&lt;p&gt;You probably want to hide the share button for non-native apps, as they have to use the browser’s share functionality and the button won’t have any functionality when clicked (as it only triggers the bridge message). You can do this by checking if view is rendered for a native app:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hotwire_native_app?&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bridge--share&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bridge--share#open&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Share&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can also add css classes to hide functionality for non-native apps. This has the added benefit of having less code in your views, and means there is only one version of the page, which makes caching a lot simpler. &lt;a href=&quot;https://blog.corsego.com/hotwire-native-conditional-tailwindcss&quot;&gt;This article by Yaroslav Shmarov&lt;/a&gt; explains how to do this with Tailwind CSS.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If anyone has written the Android variant of this component, please let me know! I didn’t dive into Android development yet, so haven’t written it yet myself.&lt;/em&gt;&lt;/p&gt;</content><author><name>Dennis Paagman</name><email>dennis@paagman.dev</email></author><category term="hotwire-native" /></entry><entry><title type="html">Benchmarking return data types</title><link href="https://paagman.dev/benchmarking-return-data-types/" rel="alternate" type="text/html" title="Benchmarking return data types" /><published>2024-01-16T00:00:00+01:00</published><updated>2024-01-16T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2024-01-16-benchmarking-return-data-types.md</id><content type="html" xml:base="https://paagman.dev/benchmarking-return-data-types/">&lt;p&gt;&lt;a href=&quot;/structuring-return-data/&quot;&gt;In a previous article&lt;/a&gt;, I discussed several approaches to handling returned data in API responses. These ranged from using plain Ruby hashes to more type-strict solutions like &lt;code class=&quot;highlighter-rouge&quot;&gt;dry-struct&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I also wanted to benchmark all of them to compare their performance. In any real-world application, the raw performance of each method is largely irrelevant, as the overhead of the web request and response time of the API itself is likely much larger by multiple orders of magnitude. However, running some benchmarks can still be a fun exercise.&lt;/p&gt;

&lt;p&gt;First, I want to formulate some assumptions about what I would expect. This is a valuable exercise for improving your ability to make ballpark estimations and train your intuition when it comes to performance.&lt;/p&gt;

&lt;p&gt;So, here are my predictions:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Nothing beats the performance of Hash, simply for the fact that all other structures need a hash as input, so per definition that’s the absolute lower limit of performance.&lt;/li&gt;
  &lt;li&gt;Hashes with symbolized keys or indifferent access (both string and symbol) are a bit slower, but not twice as slow. So let’s end in the middle and guess 1.5 times slower.&lt;/li&gt;
  &lt;li&gt;All the Ruby class like structures have roughly the same performance and are a couple of times slower than Hash.&lt;/li&gt;
  &lt;li&gt;OpenStruct performs very bad, because I already know this before I started on this article and the docs are so explicit about it. It performs the worst of all structures, about 10 times as slow.&lt;/li&gt;
  &lt;li&gt;The structures that have some form of type checking are also slower because there’s a lot more going on. It needs to check types and cast types for example. I expect those to be twice as slow as the regular class like structures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ve decided to split the tests into two parts, one where I test parsing JSON into hashes (with string keys, symbol keys and indifferent access), and one for converting those hashes into more complex structures. The reason for this is that all the other methods need a Hash as input anyway, so comparing pure performance make no sense, and I feel there is a distinct different between a hash structure and a more refined object-like structures.&lt;/p&gt;

&lt;p&gt;I also want to have a look at memory usage, just to see if anything interesting shows up there. Memory usage might be a better to reason to use one or the other in any real life application, more so than raw performance, as API responses can be quite large.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;💎 I ran these tests on a 2021 Macbook Pro equipped with a M1 Max chip and 32 GB of ram. I used Ruby 3.3.0 with YJIT enabled. (ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23])&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For these tests I convert the full output of the &lt;a href=&quot;https://api.github.com/users/dennispaagman&quot;&gt;Github User API&lt;/a&gt; for my personal account, which has about 22 simple string/integer fields in it. I wrote (with some help from Github Copilot) simple wrapper classes that take all fields as attributes. For the structures that have types I set up the correct types.&lt;/p&gt;

&lt;p&gt;First the results for hash conversion:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Hash               164.407k (± 1.4%) i/s -    837.318k in   5.093985s
Hash w/ symbols    147.386k (± 1.4%) i/s -    738.750k in   5.013394s
Hash w/ ind. xs    115.527k (± 0.6%) i/s -    586.143k in   5.073812s

Comparison:
Hash:              164407.2 i/s
Hash w/ symbols:   147386.3 i/s - 1.12x  slower
Hash w/ ind. xs:   115527.4 i/s - 1.42x  slower

Hash:                  4328 allocated
Hash w/ symbols:       4488 allocated - 1.04x more
Hash w/ ind. xs:       5360 allocated - 1.24x more
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I expected the symbolized keys and indifferent access hashes to be about 1.5 times slower, so these results are about what I expected. Memory wise there are a few more allocations, but nothing serious.&lt;/p&gt;

&lt;p&gt;Now onto the more interesting data structures.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                PORO      1.615M (± 0.7%) i/s -      8.209M in   5.083925s
              Struct    979.275k (± 2.1%) i/s -      4.991M in   5.099120s
          OpenStruct    216.668  (±18.5%) i/s -      1.072k in   5.107021s
                Data    996.112k (± 3.0%) i/s -      4.989M in   5.013517s
         Dry::Struct    179.454k (± 2.5%) i/s -    914.175k in   5.097592s
         ActiveModel    125.635k (± 0.8%) i/s -    638.612k in   5.083395s
        Hashie::Mash     61.640k (± 1.2%) i/s -    314.132k in   5.097024s
        Hashie::Dash    132.436k (± 2.0%) i/s -    663.850k in   5.014691s

Comparison:
                PORO:  1614715.8 i/s
                Data:   996112.0 i/s - 1.62x  slower
              Struct:   979275.1 i/s - 1.65x  slower
         Dry::Struct:   179454.0 i/s - 9.00x  slower
        Hashie::Dash:   132436.3 i/s - 12.19x  slower
         ActiveModel:   125634.8 i/s - 12.85x  slower
        Hashie::Mash:    61639.7 i/s - 26.20x  slower
          OpenStruct:      216.7 i/s - 7452.49x  slower

                PORO:        320 allocated
              Struct:       1232 allocated - 3.85x more
                Data:       1232 allocated - 3.85x more
         ActiveModel:       1856 allocated - 5.80x more
        Hashie::Dash:       3808 allocated - 11.90x more
          OpenStruct:      18256 allocated - 57.05x more
         Dry::Struct:      24170 allocated - 75.53x more
        Hashie::Mash:     183756 allocated - 574.24x more
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The Plain Ruby Object is the clear winner here. In general you could say that Data and Struct perform the same, being a bit slower than a simple Ruby object, but not as much as the more complex structures.&lt;/p&gt;

&lt;p&gt;We can also group together the more complex structures like Dry::Struct, regular Hashie and ActiveModel. They perform a few times slower than the simpler structures, but still seem to be pretty fast. Dry::Struct does use a lot more memory though, I assume this is because of the type checking and type casting it needs to do.&lt;/p&gt;

&lt;p&gt;The only real outlier is OpenStruct, which is a whopping &lt;strong&gt;2500 times slower (!)&lt;/strong&gt; and uses &lt;strong&gt;57 times more memory&lt;/strong&gt;. Next to that, Hashie::Mash is also &lt;em&gt;really&lt;/em&gt; memory hungry, needing almost than 575 times more allocations than a simple Ruby object.&lt;/p&gt;

&lt;p&gt;I think the main conclusion to draw from these benchmarks are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;There is no reason to ever use OpenStruct.&lt;/li&gt;
  &lt;li&gt;Any other structure is probably fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To circle back to my predictions I made at the start of the article:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;✅ Hash is the fastest (that was easy 😅).&lt;/li&gt;
  &lt;li&gt;✅ Stringified keys is a bit slower, but not 2 times.&lt;/li&gt;
  &lt;li&gt;✅ Regular Ruby objects perform about the same.&lt;/li&gt;
  &lt;li&gt;❌ OpenStruct is not 10 times slower, but 2500 times.&lt;/li&gt;
  &lt;li&gt;❌ Typed structures are not 2 times slower, but more like between 5 and 10 times.&lt;/li&gt;
&lt;/ul&gt;</content><author><name>Dennis Paagman</name><email>dennis@paagman.dev</email></author><category term="ruby" /></entry><entry><title type="html">Embedding Superset dashboards in React applications</title><link href="https://paagman.dev/embedding-superset-dashboards-in-react-applications/" rel="alternate" type="text/html" title="Embedding Superset dashboards in React applications" /><published>2024-01-10T00:00:00+01:00</published><updated>2024-01-10T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2024-01-10-react-superset.md</id><content type="html" xml:base="https://paagman.dev/embedding-superset-dashboards-in-react-applications/">&lt;p&gt;&lt;a href=&quot;https://superset.apache.org/&quot;&gt;Superset&lt;/a&gt; is an open-source data visualization platform, while &lt;a href=&quot;https://preset.io/&quot;&gt;Preset&lt;/a&gt; is a hosted SaaS solution for Superset. A notable capability of Superset is its ability to embed dashboards within other applications. This feature allows for easy dashboard management using Superset, without requiring users to leave your application. Additionally, Superset offers security rules to ensure data is correctly filtered for each user or team.&lt;/p&gt;

&lt;p&gt;To embed dashboards, Superset &lt;a href=&quot;https:// github.com/apache/superset/tree/master/superset-embedded-sdk&quot;&gt;provides an SDK&lt;/a&gt; that renders an iframe within your application. This process requires a special guest token, which you can generate on your application’s backend by interacting with the Superset API.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article presumes that you’ve already set up and configured Superset and written the backend code to generate guest tokens. If not, please &lt;a href=&quot;https://docs.preset.io/docs/dashboard-embedding&quot;&gt;refer to the official Preset (or Superset) documentation&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The primary challenge I encountered while integrating Superset into our React application was making sure I called the embed code at the right moment and only once. The library and needs access to a DOM element to insert the iframe. However, it wouldn’t render correctly when other parts of the page were still loading (and the mounting point wouldn’t exist yet) and would suffer from unnecessary additional calls caused by re-renders.&lt;/p&gt;

&lt;p&gt;My initial attempts used just &lt;code class=&quot;highlighter-rouge&quot;&gt;useEffect&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;useLayoutEffect&lt;/code&gt; hooks, which didn’t solve the problems consistently. My next attempt involved &lt;code class=&quot;highlighter-rouge&quot;&gt;useRef&lt;/code&gt; with a reference to the mounting point. However, &lt;code class=&quot;highlighter-rouge&quot;&gt;useRef&lt;/code&gt; does not trigger re-renders by design, so there’s no way to re-trigger the effect once the current reference is set after the element is added to the DOM.&lt;/p&gt;

&lt;p&gt;Luckily, there’s an alternative process where we can pass a callback function directly to &lt;code class=&quot;highlighter-rouge&quot;&gt;ref&lt;/code&gt;. This method, known as a &lt;a href=&quot;https://react.dev/reference/react-dom/components/common#ref-callback&quot;&gt;“ref callback function”&lt;/a&gt;, passes the DOM node you set the &lt;code class=&quot;highlighter-rouge&quot;&gt;ref&lt;/code&gt; on as an argument:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;When the &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;div&amp;gt;&lt;/code&gt; DOM node is added to the screen, React will call your &lt;code class=&quot;highlighter-rouge&quot;&gt;ref&lt;/code&gt; callback with the DOM &lt;code class=&quot;highlighter-rouge&quot;&gt;node&lt;/code&gt; as the argument.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class=&quot;language-tsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This perfectly suits our needs as it will only be called when the element is available in the DOM.&lt;/p&gt;

&lt;p&gt;The final point to address is to make sure that the embedding code is called only once, not every time the component is re-rendered, for example when a parent gets re-rendered. This can be achieved by keeping a simple ‘mounted’ state, which can then be used to make sure the code gets called only once.&lt;/p&gt;

&lt;p&gt;Combining these elements, we can create a custom hook. This custom hook takes one argument, the &lt;code class=&quot;highlighter-rouge&quot;&gt;id&lt;/code&gt; of the dashboard, and returns a callback function that takes the mounting point as the sole argument. This function can then be passed into the &lt;code class=&quot;highlighter-rouge&quot;&gt;ref&lt;/code&gt; as the direct callback function in your component:&lt;/p&gt;

&lt;div class=&quot;language-tsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useCallback&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;embedDashboard&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@superset-ui/embedded-sdk&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Implement your actual API backend call here (through a Promise).&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fetchGuestToken&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/* ... */&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useSupersetEmbed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mounted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setMounted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ref&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;useRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;nf&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mounted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;embedDashboard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;supersetDomain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://....&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;mountPoint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;fetchGuestToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// dashboardUiConfig: {},&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// debug: true,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;setMounted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mounted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SupersetExampleDashboard&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;embed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;useSupersetEmbed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;your-dashboard-id-here&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;embed&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And that’s it! You can now embed Superset dashboards in your React application without any reloading or rendering issues.&lt;/p&gt;</content><author><name>Dennis Paagman</name><email>dennis@paagman.dev</email></author><category term="react" /></entry></feed>