Bridgetown2025-12-30T06:34:20+00:00https://marcoroth.dev/feed.xmlMarco Rothmarcoroth.dev - website and blogGlamorous Christmas: Bringing Charm to Ruby2025-12-25T11:00:00+00:002025-12-25T11:00:00+00:00repo://posts.collection/_posts/2025-12-25-glamorous-christmas.md<p>Today, <a href="https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/">Ruby 4.0 was released</a>. What an exciting milestone for the language!</p>
<p>This release brings some amazing new features like the experimental <code>Ruby::Box</code> isolation mechanism, the new ZJIT compiler, significant performance improvements for class instantiation, and promotions of <code>Set</code> and <code>Pathname</code> to core classes. It’s incredible to see how Ruby continues to thrive and be pushed forward 30 years after its first release.</p>
<p>To celebrate this release, I’m happy to announce that I’ve been working on porting the <a href="https://charm.sh">Charmbracelet</a> Go terminal libraries to Ruby, and today I’m releasing a first version of them. What better way to make this Ruby 4.0 release a little more <em>glamorous</em> and <em>charming</em>?</p>
<p>I’ve been a huge fan of the Charm terminal libraries for years. I’ve always loved the aesthetics and the look & feel of the Charm libraries. Every time I see a CLI tool built with them, I get a little envious. The attention to detail, the smooth animations, the gorgeous styling, it all just <em>feels</em> right. I’ve spent more time than I’d like to admit just playing around with their demos and admiring how polished everything looks.</p>
<p>For a long time, I wished we had something like this in Ruby. So I finally decided to stop wishing and start building. Spoiler: some of these are pure Ruby ports, while others are bindings to their Go-library counterparts.</p>
<hr />
<h2 id="what-is-charm">What is Charm?</h2>
<p><a href="https://charm.sh">Charmbracelet</a> is an open source project building tools for the terminal. Their tagline is <em>“We make the command line glamorous.”</em>, and they truly deliver on that promise.</p>
<p>The Charm team believes that developer experience is user experience. APIs and CLIs are user interfaces at the end of the day, and they deserve the same care and attention as any visual design. This philosophy has led them to build some of the most beautiful and well-designed terminal libraries in any language.</p>
<p>What sets Charm apart is that their tools make people <em>feel</em> something. When you see a Charm-powered application for the first time, there’s a moment of delight, a “wow, I didn’t know the terminal could look like this.” That’s not an accident. It’s the result of caring deeply about craft and aesthetics in a space where most tools are purely functional.</p>
<p>One of their key insights was separating structure from style, mirroring how the web separates HTML from CSS. Lipgloss handles styling and layout, while Bubble Tea provides the application architecture. This separation makes it easy to build complex UIs without mixing concerns.</p>
<h2 id="the-charm-ecosystem">The Charm Ecosystem</h2>
<p>The Charm libraries are thoughtfully designed with composable pieces that work together beautifully:</p>
<ul>
<li><strong><a href="https://github.com/charmbracelet/lipgloss">Lipgloss</a></strong> provides the styling primitives like colors, borders, padding, and alignment</li>
<li><strong><a href="https://github.com/charmbracelet/bubbletea">Bubble Tea</a></strong> gives you the Elm-inspired Model-View-Update architecture for building interactive TUIs</li>
<li><strong><a href="https://github.com/charmbracelet/bubbles">Bubbles</a></strong> offers ready-to-use components built on Bubble Tea</li>
<li><strong><a href="https://github.com/charmbracelet/glamour">Glamour</a></strong> renders Markdown gorgeously in the terminal</li>
<li><strong><a href="https://github.com/charmbracelet/huh">Huh?</a></strong> makes building interactive forms a breeze</li>
<li><strong><a href="https://github.com/charmbracelet/harmonica">Harmonica</a></strong> provides spring-based animations and easing functions</li>
<li><strong><a href="https://github.com/charmbracelet/gum">Gum</a></strong> democratizes interactive UIs for shell scripts</li>
</ul>
<p>Each library solves one problem well, and together they form a complete toolkit for building beautiful terminal applications.</p>
<hr />
<h2 id="why-port-charm-to-ruby">Why Port Charm to Ruby?</h2>
<p>Ruby has always been a joy to work with. They say Ruby is a programmer’s best friend, and I think that’s true. For 30 years, the language has embodied a philosophy of making coding more human, intuitive, and enjoyable. Matz built Ruby around the Principle of Least Surprise, valuing craftsmanship and expressive code over hype-driven trends. It’s a language that cares about how things <em>feel</em>.</p>
<p>Charm shares that same DNA. Their tools prioritize developer happiness, expressiveness, and craft. When I discovered Charm, I immediately thought: this belongs in Ruby.</p>
<p>We have great tools for building CLIs, but when it comes to building rich, interactive, <em>beautiful</em> terminal applications, we’ve been missing the kind of cohesive ecosystem that Charm provides for Go.</p>
<p>I wanted to change that. Ruby developers deserve glamorous terminals too. I want Ruby developers to build terminal applications so beautiful that even people who “don’t like CLIs” find themselves captivated.</p>
<p>These ports aim to bring that same level of polish and developer experience to Ruby. Quick script or full-blown TUI app, you should be able to make it look stunning with minimal effort.</p>
<hr />
<h2 id="a-renaissance-in-ruby-developer-experience">A Renaissance in Ruby Developer Experience</h2>
<p>This project is part of a broader movement happening in the Ruby community. Over the past few years, we’ve seen an incredible focus on improving the developer experience, and it’s been driven by a key insight: <strong>strategic standardization enables innovation</strong>.</p>
<p>Just as Rails revolutionized web development by standardizing conventions, the Ruby ecosystem is now applying the same principle to developer tooling. By standardizing foundational layers, we free ourselves to build richer, more powerful tools on top.</p>
<p>This extends beyond individual tools. Protocols like <a href="https://microsoft.github.io/language-server-protocol/">LSP (Language Server Protocol)</a>, <a href="https://microsoft.github.io/debug-adapter-protocol/">DAP (Debug Adapter Protocol)</a>, and now <a href="https://modelcontextprotocol.io/">MCP (Model Context Protocol)</a> provide shared infrastructure that unifies how tools communicate. Instead of every editor implementing language support from scratch, LSP gives us a common language. Instead of every debugger reinventing the wheel, DAP provides a shared foundation. These protocols enable an explosion of interoperable tooling.</p>
<p>And that’s the key insight: when you build strong foundations, the community builds incredible things on top. Give developers the right building blocks, and they’ll create tools you never imagined.</p>
<p>This story extends across the Ruby ecosystem:</p>
<ul>
<li><strong><a href="https://github.com/ruby/prism">Prism</a></strong>, Ruby’s new official parser designed for tooling from the ground up, providing a standard foundation for static analysis</li>
<li><strong><a href="https://github.com/Shopify/ruby-lsp">Ruby LSP</a></strong>, a language server and platform for editor intelligence with a growing ecosystem of add-ons</li>
<li><strong><a href="https://sorbet.org">Sorbet</a></strong>, a fast, powerful type checker for Ruby</li>
<li><strong><a href="https://github.com/ruby/rbs">RBS</a></strong>, Ruby’s type signature language standardizing how we describe types</li>
<li><strong><a href="https://github.com/soutaro/rbs-inline">RBS Inline</a></strong>, inline type annotations directly in Ruby source files</li>
<li><strong><a href="https://github.com/soutaro/steep">Steep</a></strong>, a gradual type checker built on RBS</li>
<li><strong><a href="https://github.com/ruby/debug">debug</a></strong>, Ruby’s modern debugger with IDE integration</li>
<li><strong><a href="https://github.com/ruby/irb">IRB</a></strong> and <strong><a href="https://github.com/ruby/rdoc">RDoc</a></strong>, core tools receiving renewed attention and improvements</li>
<li><strong><a href="https://herb-tools.dev">Herb</a></strong>, an HTML-aware ERB parser and ecosystem enabling better tooling for HTML views and rendering</li>
</ul>
<p>The Ruby ecosystem is investing heavily in making the development experience more polished, more intelligent, and more delightful. From better editor support to faster parsers to type checking, Ruby is leveling up.</p>
<p>Bringing Charm to Ruby fits right into this story. It’s about raising the bar for what Ruby developers can build and how good it can look. The terminal is often where we spend our time as developers, and it deserves the same attention to developer experience as our editors and IDEs.</p>
<hr />
<h2 id="the-libraries">The Libraries</h2>
<h3 id="lipgloss"><a href="https://github.com/marcoroth/lipgloss-ruby">Lipgloss</a></h3>
<p>CSS-like styling for terminal output. Define colors, borders, padding, margins, and alignment with a clean, chainable API. Includes support for tables, lists, and tree structures.</p>
<h4 id="example">Example</h4>
<pre><code class="language-ruby">require "lipgloss"
style = Lipgloss::Style.new
.bold(true)
.foreground("#FAFAFA")
.background("#7D56F4")
.padding(1, 2)
.border(:rounded)
.border_foreground("#874BFD")
puts style.render("Hello, Glamorous Ruby!")
</code></pre>
<h4 id="demo">Demo</h4>
<p><img src="/images/glamorous-christmas/lipgloss.png" alt="Lipgloss demo" loading="lazy" /></p>
<hr />
<h3 id="bubble-tea"><a href="https://github.com/marcoroth/bubbletea-ruby">Bubble Tea</a></h3>
<p>The Elm-inspired TUI framework. Build interactive terminal applications using the Model-View-Update architecture.</p>
<h4 id="example-1">Example</h4>
<pre><code class="language-ruby">require "bubbletea"
class Counter
include Bubbletea::Model
def initialize
@count = 0
end
def init
[self, nil]
end
def update(message)
case message
when Bubbletea::KeyMessage
case message.to_s
when "q", "ctrl+c"
[self, Bubbletea.quit]
when "up", "k"
@count += 1
[self, nil]
when "down", "j"
@count -= 1
[self, nil]
else
[self, nil]
end
else
[self, nil]
end
end
def view
"Count: #{@count}\n\nPress up/down to change, q to quit"
end
end
Bubbletea.run(Counter.new)
</code></pre>
<h4 id="demos">Demos</h4>
<video src="/images/glamorous-christmas/bubbletea-package-manager.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<video src="/images/glamorous-christmas/bubbletea-fancy-list.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<video src="/images/glamorous-christmas/bubbletea-test-runner.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<hr />
<h3 id="bubbles"><a href="https://github.com/marcoroth/bubbles-ruby">Bubbles</a></h3>
<p>Pre-built TUI components for Bubble Tea: spinners, progress bars, text inputs, text areas, viewports, lists, tables, file pickers, and more.</p>
<h4 id="example-2">Example</h4>
<pre><code class="language-ruby">require "bubbles"
</code></pre>
<p><strong>Animated spinner</strong></p>
<pre><code class="language-ruby">spinner = Bubbles::Spinner.new
spinner.spinner = Bubbles::Spinners::DOT
</code></pre>
<p><strong>Progress bar</strong></p>
<pre><code class="language-ruby">progress = Bubbles::Progress.new(width: 40)
progress.set_percent(0.5)
</code></pre>
<p><strong>Text input</strong></p>
<pre><code class="language-ruby">input = Bubbles::TextInput.new
input.placeholder = "Enter your name..."
input.focus
</code></pre>
<h4 id="demos-1">Demos</h4>
<video src="/images/glamorous-christmas/bubbles-filepicker.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<video src="/images/glamorous-christmas/bubbles-progress.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<video src="/images/glamorous-christmas/bubbles-stopwatch.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<video src="/images/glamorous-christmas/bubbles-viewport.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<hr />
<h3 id="glamour"><a href="https://github.com/marcoroth/glamour-ruby">Glamour</a></h3>
<p>Stylesheet-based Markdown rendering for the terminal. Supports syntax highlighting, multiple themes, and custom styles via a Ruby DSL.</p>
<h4 id="example-3">Example</h4>
<pre><code class="language-ruby">require "glamour"
markdown = <<~MD
# Hello, World!
This is **bold** and this is *italic*.
- Item one
- Item two
- Item three
MD
puts Glamour.render(markdown, style: "dark", width: 80)
</code></pre>
<h4 id="demos-2">Demos</h4>
<p><img src="/images/glamorous-christmas/glamour-basic.png" alt="Glamour basic" loading="lazy" /></p>
<p><img src="/images/glamorous-christmas/glamour-tables.png" alt="Glamour tables" loading="lazy" /></p>
<p><img src="/images/glamorous-christmas/glamour-renderer.png" alt="Glamour renderer" loading="lazy" /></p>
<hr />
<h3 id="huh"><a href="https://github.com/marcoroth/huh-ruby">Huh?</a></h3>
<p>A simple, powerful library for building interactive forms and prompts. Includes inputs, selects, multi-selects, confirms, spinners, validation, and theming.</p>
<h4 id="example-4">Example</h4>
<pre><code class="language-ruby">require "huh"
form = Huh.form(
Huh.group(
Huh.input
.key("name")
.title("What's your name?")
.placeholder("Enter your name..."),
Huh.select
.key("color")
.title("Favorite color?")
.options(
Huh.option("Red", "red"),
Huh.option("Green", "green"),
Huh.option("Blue", "blue")
),
Huh.confirm
.key("ready")
.title("Ready to continue?")
)
)
form.run
puts "Hello, #{form["name"]}!"
</code></pre>
<h4 id="demos-3">Demos</h4>
<video src="/images/glamorous-christmas/huh-multigroup.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<video src="/images/glamorous-christmas/huh-validation.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<hr />
<h3 id="harmonica"><a href="https://github.com/marcoroth/harmonica-ruby">Harmonica</a></h3>
<p>A simple, physics-based animation library. Damped spring oscillators for smooth, natural motion in your terminal UIs.</p>
<h4 id="example-5">Example</h4>
<pre><code class="language-ruby">require "harmonica"
spring = Harmonica::Spring.new(
delta_time: Harmonica.fps(60),
angular_frequency: 6.0,
damping_ratio: 0.5
)
position = 0.0
velocity = 0.0
target = 100.0
loop do
position, velocity = spring.update(position, velocity, target)
break if (position - target).abs < 0.01
end
</code></pre>
<h4 id="demos-4">Demos</h4>
<video src="/images/glamorous-christmas/harmonica-spring.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<video src="/images/glamorous-christmas/harmonica-damping.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<hr />
<h3 id="bubblezone"><a href="https://github.com/marcoroth/bubblezone-ruby">Bubblezone</a></h3>
<p>Mouse event zones for TUIs. Mark regions as clickable and easily determine which component was clicked in your Bubble Tea applications.</p>
<h4 id="example-6">Example</h4>
<pre><code class="language-ruby">require "bubblezone"
</code></pre>
<p><strong>Mark regions with zone identifiers</strong></p>
<pre><code class="language-ruby">ok_button = Bubblezone.mark("confirm", styled_ok_button)
cancel_button = Bubblezone.mark("cancel", styled_cancel_button)
</code></pre>
<p><strong>Check if mouse event is in bounds</strong></p>
<pre><code class="language-ruby">if Bubblezone.get("confirm").in_bounds?(mouse_message)
# Handle confirm click
end
</code></pre>
<h4 id="demo-1">Demo</h4>
<video src="/images/glamorous-christmas/bubblezone.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<hr />
<h3 id="gum"><a href="https://github.com/marcoroth/gum-ruby">Gum</a></h3>
<p>A tool for glamorous shell scripts. Ruby wrapper for Charm’s gum binary with an idiomatic API for inputs, selects, confirms, spinners, styled output, and more.</p>
<h4 id="example-7">Example</h4>
<pre><code class="language-ruby">require "gum"
</code></pre>
<p><strong>Text input</strong></p>
<pre><code class="language-ruby">name = Gum.input(placeholder: "Enter your name")
</code></pre>
<p><img src="/images/glamorous-christmas/gum-input.png" alt="Gum input" loading="lazy" /></p>
<p><strong>Selection menu</strong></p>
<pre><code class="language-ruby">color = Gum.choose("red", "green", "blue")
</code></pre>
<p><img src="/images/glamorous-christmas/gum-choose.png" alt="Gum choose" loading="lazy" /></p>
<p><strong>Confirmation prompt</strong></p>
<pre><code class="language-ruby">if Gum.confirm("Continue?")
Gum.spin("Processing...") { do_work }
end
</code></pre>
<video src="/images/glamorous-christmas/gum-confirm.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<p><strong>Styled output</strong></p>
<pre><code class="language-ruby">puts Gum.style("Done!", foreground: "212", bold: true, border: :rounded)
</code></pre>
<p><img src="/images/glamorous-christmas/gum-style.png" alt="Gum style" loading="lazy" /></p>
<hr />
<h3 id="ntcharts"><a href="https://github.com/marcoroth/ntcharts-ruby">ntcharts</a></h3>
<p>Nimble Terminal Charts. Visualize data beautifully with sparklines, bar charts, line charts, time series, heatmaps, and more.</p>
<h4 id="example-8">Example</h4>
<pre><code class="language-ruby">require "ntcharts"
sparkline = Ntcharts::Sparkline.new(10, 5)
sparkline.push_all([7.8, 3.8, 8.4, 2.1, 4.2, 6.8, 2.5, 9.2, 1.3])
sparkline.draw
puts sparkline.view
</code></pre>
<h4 id="demos-5">Demos</h4>
<div class="grid grid-cols-2 gap-4 not-prose p-4 rounded-lg" style="background-color: #282c32;">
<img src="/images/glamorous-christmas/ntcharts-barchart.png" alt="" loading="lazy" />
<img src="/images/glamorous-christmas/ntcharts-horizontal-barchart.png" alt="" loading="lazy" />
<img src="/images/glamorous-christmas/ntcharts-barchart-series.png" alt="" loading="lazy" />
<img src="/images/glamorous-christmas/ntcharts-braille-lines.png" alt="" loading="lazy" />
<img src="/images/glamorous-christmas/ntcharts-braille-rendering.png" alt="" loading="lazy" />
<img src="/images/glamorous-christmas/ntcharts-sine-wave.png" alt="" loading="lazy" />
<img src="/images/glamorous-christmas/ntcharts-multiple-time-series.png" alt="" loading="lazy" class="col-span-2" />
</div>
<video src="/images/glamorous-christmas/ntcharts-streaming.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<hr />
<h2 id="installation">Installation</h2>
<p>All gems (except <code>huh</code>) are available on RubyGems.org:</p>
<pre><code class="language-bash">gem install lipgloss
gem install bubbletea
gem install bubbles
gem install glamour
gem install harmonica
gem install bubblezone
gem install gum
gem install ntcharts
</code></pre>
<p>For <code>huh</code>, add it to your Gemfile from GitHub (we reached out to the current owner of the <code>huh</code> gem and are hoping to be able to publish the gem under the <code>huh</code> name in the future):</p>
<pre><code class="language-ruby">gem "huh", github: "marcoroth/huh-ruby"
</code></pre>
<hr />
<h2 id="source-code">Source Code</h2>
<p>You can find more information at <a href="https://charm-ruby.dev">charm-ruby.dev</a>. All libraries are open source and available on GitHub:</p>
<table>
<thead>
<tr>
<th>Library</th>
<th>Gem</th>
<th>Repository</th>
</tr>
</thead>
<tbody>
<tr>
<td>Lipgloss</td>
<td><code>lipgloss</code></td>
<td><a href="https://github.com/marcoroth/lipgloss-ruby">github.com/marcoroth/lipgloss-ruby</a></td>
</tr>
<tr>
<td>Bubble Tea</td>
<td><code>bubbletea</code></td>
<td><a href="https://github.com/marcoroth/bubbletea-ruby">github.com/marcoroth/bubbletea-ruby</a></td>
</tr>
<tr>
<td>Bubbles</td>
<td><code>bubbles</code></td>
<td><a href="https://github.com/marcoroth/bubbles-ruby">github.com/marcoroth/bubbles-ruby</a></td>
</tr>
<tr>
<td>Bubblezone</td>
<td><code>bubblezone</code></td>
<td><a href="https://github.com/marcoroth/bubblezone-ruby">github.com/marcoroth/bubblezone-ruby</a></td>
</tr>
<tr>
<td>Glamour</td>
<td><code>glamour</code></td>
<td><a href="https://github.com/marcoroth/glamour-ruby">github.com/marcoroth/glamour-ruby</a></td>
</tr>
<tr>
<td>Gum</td>
<td><code>gum</code></td>
<td><a href="https://github.com/marcoroth/gum-ruby">github.com/marcoroth/gum-ruby</a></td>
</tr>
<tr>
<td>Harmonica</td>
<td><code>harmonica</code></td>
<td><a href="https://github.com/marcoroth/harmonica-ruby">github.com/marcoroth/harmonica-ruby</a></td>
</tr>
<tr>
<td>Huh?</td>
<td><code>huh</code></td>
<td><a href="https://github.com/marcoroth/huh-ruby">github.com/marcoroth/huh-ruby</a></td>
</tr>
<tr>
<td>Nimble Terminal Charts</td>
<td><code>ntcharts</code></td>
<td><a href="https://github.com/marcoroth/ntcharts-ruby">github.com/marcoroth/ntcharts-ruby</a></td>
</tr>
</tbody>
</table>
<hr />
<h2 id="whats-next">What’s Next</h2>
<p>This is just the beginning. These are initial releases, and there’s plenty more to do.</p>
<p>The first pass was primarily focused on getting the Go functionality ported to Ruby. Now the real work begins: making these libraries feel more Ruby-like and idiomatic. I want to improve how the libraries work and integrate with each other, add more pre-built components, and make it even easier to get started building beautiful terminal applications.</p>
<p>Some areas I’m focusing on:</p>
<ul>
<li>Making the APIs more idiomatic and Ruby-like</li>
<li>Improving integration between the libraries</li>
<li>Expanding the component libraries with more ready-to-use pieces</li>
<li>Better documentation and examples</li>
<li>Performance optimizations</li>
</ul>
<p>These are early releases, and there’s still work to be done. Some of the Go-based gems have quirks when used together due to how Goroutines are initialized. I’m actively working on these, and I’d rather build in public with the community than wait for everything to be perfect.</p>
<hr />
<h2 id="lets-build-together">Let’s Build Together</h2>
<p>But here’s what really excites me: imagine what we could build together. Think about all the CLI tools, test runners, generators, installers, and framework scripts we use every day.</p>
<p>What if they were all a little more polished, a little more interactive, a little more <em>glamorous</em>? Rails generators with beautiful progress indicators. Test runners with live, filterable output. Bundler with elegant spinners and styled summaries. Or entirely new applications that weren’t really possible before, but now are. The possibilities are endless!</p>
<p>I couldn’t wait to start exploring, so here are a couple of things I’ve been experimenting with:</p>
<h3 id="minitest-bubble-tea-runner">Minitest Bubble Tea Runner</h3>
<p>A Minitest plugin that uses Bubble Tea to run your tests with a beautiful, interactive interface. Inspired by <a href="https://vitest.dev">Vitest</a>’s terminal UI. The UI stays open after the run completes, letting you re-run the failed tests, filter to focus on specific tests, and enjoy a clean collapsible view that tucks away individual test cases when a file passes. (This is an extension of the static standalone Bubble Tea test runner demo in the <a href="#bubble-tea">Bubble Tea demos</a> above.)</p>
<video src="/images/glamorous-christmas/minitest-bubbletea.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<h3 id="herb-tui-playground">Herb TUI Playground</h3>
<p>A terminal rebuild of the <a href="https://herb-tools.dev/playground">Herb Playground</a>, built entirely with these libraries. Type HTML+ERB code and switch between tabs to explore the parsed AST, tokens, extracted Ruby, extracted HTML, and any parser errors, all in a beautiful split-pane terminal UI.</p>
<video src="/images/glamorous-christmas/herb-playground-tui.mp4" autoplay="" loop="" muted="" playsinline=""></video>
<h2 id="thank-you">Thank You</h2>
<p>A special thank you to <a href="https://github.com/webgago">Anton Sozontov</a> for transferring the <code>gum</code> gem name on RubyGems.org, and to <a href="https://www.eq8.eu">Tomas Valent</a> for transferring the <code>bubbles</code> gem name.</p>
<p>The terminal has been around for decades, but that doesn’t mean it has to feel dated. With the right tools, you can build software that genuinely delights.</p>
<p>I hope these libraries inspire you to craft something that makes people stop and say “wow.”</p>
<p>But more than anything, I want to encourage you to build with these libraries. Build new tools. Upgrade your existing CLI applications to make them shinier, more glamorous, more beautiful. Add some color, some polish, some fun.</p>
<p>The terminal doesn’t have to be boring. Let’s make it glamorous.</p>
<p>I’d love to hear your feedback and see what you create! You can find all the libraries at <a href="https://charm-ruby.dev">charm-ruby.dev</a> and browse the full collection on my <a href="https://github.com/stars/marcoroth/lists/charm-ruby">GitHub list</a>.</p>
<p>Happy Ruby 4.0 release day! Let’s make the Ruby command line glamorous.</p>
<p>— Marco</p>Marco RothGiving Back to the Rails Community2025-12-18T14:30:00+00:002025-12-18T14:30:00+00:00repo://posts.collection/_posts/2025-12-18-rails-luminary-2025.md<p>I’m deeply grateful and truly honored to be named a <a href="https://rubyonrails.org/2025/12/17/marco-roth-2025-rails-luminary"><strong>Rails Luminary</strong></a> this year. Thank you to everyone who supported me, my work, and took the time to nominate me. Recognition like this only exists because of a community that cares about long-term contribution, and I don’t take that lightly.</p>
<p><img loading="lazy" src="/images/resized/rails-luminary-2025-1abc9e58-1280x.jpeg" data-pswp-src="/images/rails-luminary-2025/rails-luminary-2025.jpeg" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl" alt="Rails Luminary 2025 Announcement" /></p>
<p>Rails has given me a lot over the years.</p>
<p>Not just technically, but professionally and personally. Rails gave me a way to build things that matter, a community that values craft, and an environment where curiosity and care are encouraged. A community where people build, ship, and want to be productive. It helped me grow in ways I didn’t expect, including getting started with public speaking, and then getting more comfortable standing on a stage sharing knowledge and the work I’ve been doing.</p>
<p>In such a supportive and welcoming environment, giving back stopped feeling like a choice and started feeling like the natural thing to do. That’s how Rails became what it is today: people showing up, doing the work because it needed doing, smoothing out rough edges, building tools, and helping others ship. It’s part of the culture. Being part of that feels right, and being part of this community is an honor and a privilege.</p>
<p>The way I try to give back is by building things I need myself and sharing them openly, as honest attempts to solve real problems. I genuinely love open source, and I especially enjoy solving tricky problems: tooling, parsers, developer experience, and the infrastructure that quietly supports everything else.</p>
<p>Projects like <a href="https://github.com/marcoroth/herb">Herb</a>, <a href="https://github.com/marcoroth/reactionview">ReActionView</a>, <a href="/open-source">Hotwire-related tooling (Stimulus LSP, …)</a>, the <a href="https://www.hotwireweekly.com/archive/">Hotwire Weekly Newsletter</a>, and <a href="https://rubyevents.org">RubyEvents.org</a> all grew out of that mindset. They started as personal needs and became shared efforts because others showed up with feedback, issues, reviews, and ideas.</p>
<p>This year was particularly humbling. I had the privilege of traveling and speaking at conferences around the world: 18 talks across 15 countries and four continents. Being able to share my work, exchange ideas, and meet Ruby friends in so many places reminded me how global, generous, and thoughtful this community really is. I’m incredibly thankful for every conversation, question, and moment of encouragement along the way. Being able to connect with the Ruby community face to face is something very special.</p>
<p>I also want to explicitly thank <strong>Xavier Noria</strong>, <strong>Amanda Perino</strong>, and the entire <strong>Renuo</strong> team for putting on the event and for the care they bring to creating spaces like this. It shows, and it matters.</p>
<p><img loading="lazy" src="/images/resized/xavier-marco-4722f323-1280x.jpeg" data-pswp-src="/images/rails-luminary-2025/xavier-marco.jpeg" data-pswp-width="2267" data-pswp-height="2268" class="rounded-xl" alt="Xavier Noria and Marco Roth in Zurich (📷 Amanda Perino)" /></p>
<p>None of this work happens in isolation. I’m grateful to everyone who tested early versions, filed issues, challenged assumptions, shared encouragement, or quietly helped in ways that never make it into release notes or commit messages. And I’m especially thankful to those who nominated me, that trust means a lot.</p>
<p>I don’t see this award as a finish line. If anything, it’s encouragement to keep going. I plan to continue doing the work I care about, pushing on the parts of the ecosystem that can be better, and sharing what I build along the way, openly and with care.</p>
<p>Looking ahead to 2026, I’ll keep pushing on the Herb tooling, the <code>Herb::Engine</code>, and improving ReActionView as we work our way through the <a href="/posts/railsconf-2025-recap#:~:text=6%20adoption%20levels">6 adoption levels</a> I outlined earlier this year. There’s still a lot to explore, build and iterate on.</p>
<p>I’m also planning to write a full 2025 Year-in-Review post soon. If you’re curious about what’s next, feel free to follow along.</p>
<p>Thank you for the support, and for making Rails a place worth caring deeply about.</p>
<p>Marco</p>Marco RothIntroducing ReActionView: A new ActionView-Compatible ERB Engine and initiative for the Rails view layer2025-09-11T12:00:00+00:002025-09-11T12:00:00+00:00repo://posts.collection/_posts/2025-09-11-rails-world-2025-recap.md<p><a href="https://rubyonrails.org/world/2025">Rails World 2025</a> just wrapped up in Amsterdam last week. This blog post is a recap of my talk titled <a href="https://rubyonrails.org/world/2025/day-2/marco-roth"><em>“Introducing ReActionView: A new ActionView-Compatible ERB Engine”</em></a> and summarizes my talk, the new releases, and where my view-layer work is headed next.</p>
<p>This talk is the conclusion of a journey I’ve been sharing throughout 2025. At RubyKaigi, I introduced <a href="/posts/introducing-herb">Herb: a new HTML-aware ERB parser and tooling ecosystem</a>. At <a href="/posts/railsconf-2025-recap">RailsConf, I released developer tools built on Herb, including a formatter, linter, and language server</a>, alongside a vision for modernizing and improving the Rails view layer.</p>
<p><img loading="lazy" src="/images/resized/rubykaigi-railsconf-rails-world-2989159-1280x.png" data-pswp-src="/images/rails-world-2025-recap/rubykaigi-railsconf-rails-world.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="RubyKaigi, RailsConf and Rails World 2025" /></p>
<p>At Rails World, I started to deliver on the vision I shared at RailsConf. I publicly introduced <strong>ReActionView</strong>, an initiative to explore what’s possible in the Rails view layer, plus a significant update to Herb with v0.7 and <code>Herb::Engine</code>.</p>
<p><code>Herb::Engine</code> is a new ERB engine built on the Herb Parser, fully compatible with <code>.html.erb</code> but with HTML validation, better error feedback, reactive updates (soon), and built-in tooling.</p>
<p>This is tying together everything from the past talks and the work I have done so far in 2025 to show the full potential of Herb, ReActionView, and where the Rails view layer could be headed.</p>
<p><strong>Slides:</strong> <a href="https://speakerdeck.com/marcoroth/introducing-reactionview-a-new-actionview-compatible-erb-engine-at-rails-world-2025-amsterdam">Speaker Deck</a> <br />
<strong>Projects:</strong> <a href="https://github.com/marcoroth/reactionview">ReActionView</a> • <a href="https://github.com/marcoroth/herb">Herb</a> • <a href="https://github.com/marcoroth/herb/tree/main/javascript/packages/stimulus-lint#readme">Stimulus Lint</a> <br />
<strong>Recording:</strong> (<em>coming soon</em>)</p>
<hr />
<h2 id="what-we-announced">What we announced</h2>
<p><img loading="lazy" src="/images/resized/summary-91b66a01-1280x.png" data-pswp-src="/images/rails-world-2025-recap/summary.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Rails World 2025 summary slide: ReActionView, Herb::Engine, Dev Tools, Linters, and more" /></p>
<ul>
<li><strong>ReActionView initiative</strong>: a pragmatic, ActionView-compatible path forward for the view layer.</li>
<li><strong>Herb v0.7</strong>: updates across the <strong>Linter</strong>, <strong>Formatter</strong>, <strong>Language Server</strong>, and <strong>VS Code extension</strong>.</li>
<li><strong>Herb::Engine</strong>: an HTML-aware, framework-independent ERB rendering engine.</li>
<li><strong>Dev Tools for the browser</strong>: view/partial/component <strong>outlines with jump-to-source</strong>, <strong>ERB output hover</strong>, and <strong>dismissable validation overlays</strong> during development.</li>
<li><strong>New linter rules</strong>: +18 rules (9 accessibility focused), bringing the total to <strong>32</strong>.</li>
<li><strong>Stimulus Lint</strong> The extracted diagnostics from Stimulus LSP (powered by Herb).</li>
<li><strong>Tailwind class rewriter</strong> (coming soon).</li>
<li>Early experiments toward <strong>Phoenix LiveView-style reactivity</strong> for ERB.</li>
</ul>
<hr />
<h2 id="herb-linter-updates-since-railsconf">Herb Linter updates since RailsConf</h2>
<ul>
<li><strong>18 new rules</strong> (including <strong>9 accessibility</strong>), for a total <strong>32 linter rules</strong>.</li>
<li>CLI supports <code>--github</code> flag for <a href="https://github.com/rubyevents/rubyevents/pull/920/files#diff-3be6357d784895c034fb68c23f3c7afc10f76feacc0ebc7834155910b4db1316">annotating pull requests</a>.</li>
<li>More advanced control flow analysis</li>
</ul>
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/rails-world-2025-recap/linter-control-flow.png" class="" aria-label="Herb Linter Control Flow Analysis">
<source src="/images/rails-world-2025-recap/linter-control-flow.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6">Herb Linter Control Flow Analysis</figcaption>
</figure>
<h2 id="herb-formatter-updates-since-railsconf">Herb Formatter updates since RailsConf</h2>
<p>The Herb Formatter got a lot of improvements in terms of stability and predictability. We are getting really close to enabling the formatter by default in the Herb Language Server, so it formats HTML+ERB files on save.</p>
<p>Another thing we have been working on is a rewriter system for the formatter, so we can rewrite part of the document before and/or after formatting.</p>
<p>One of the most requested features is to be able to format the Tailwind CSS classes inside the <code>class</code> attribute. <a href="https://tailwindcss.com/blog/automatic-class-sorting-with-prettier">Tailwind has an official recommended way for ordering classes</a> but it’s tied to the Prettier ecosystem and doesn’t really work well with HTML+ERB documents.</p>
<p>So we extracted the algorithm into a new package <a href="https://www.npmjs.com/package/@herb-tools/tailwind-class-sorter"><code>@herb-tools/tailwind-class-sorter</code></a> that the algorithm can be used independently of the prettier plugin.</p>
<p><img loading="lazy" src="/images/resized/tailwind-class-rewriter-7b6fa962-1280x.png" data-pswp-src="/images/rails-world-2025-recap/tailwind-class-rewriter.png" data-pswp-width="1920" data-pswp-height="1080" class="" alt="" /></p>
<p>The Herb Formatter is going to have an option to integrate this package so we automatically sort the Tailwind classes when saving the document.</p>
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/rails-world-2025-recap/formatter-tailwind-class-rewriter.png" class="" aria-label="">
<source src="/images/rails-world-2025-recap/formatter-tailwind-class-rewriter.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6"></figcaption>
</figure>
<p>Since the Herb Parser understands both HTML, ERB, and Ruby it’s also going to work when you use Action View Tag Helpers like <code>tag.div</code>, <code>content_tag</code>, <code>link_to</code>, and more.</p>
<p><img loading="lazy" src="/images/resized/tailwind-class-rewriter-snippets-8dcdeb8a-1280x.png" data-pswp-src="/images/rails-world-2025-recap/tailwind-class-rewriter-snippets.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<h2 id="stimulus-lint">Stimulus Lint</h2>
<p>The <a href="https://github.com/marcoroth/stimulus-lsp">Stimulus LSP</a> has had a lot of diagnostics that help you catch errors when working with Stimulus. But these diagnostics where exclusive and locked into the Language Server implementation, so you couldn’t use them independently.</p>
<p>We now started to work on extracting these diagnostics into a new <a href="https://github.com/marcoroth/herb/tree/main/javascript/packages/stimulus-lint">Stimulus Lint</a> package, which is powered by the Herb Linter infrastructure, so it’s going to work on both HTML and HTML+ERB documents.</p>
<p><img loading="lazy" src="/images/resized/stimulus-lint-bd22ea88-1280x.png" data-pswp-src="/images/rails-world-2025-recap/stimulus-lint.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>Stimulus Lint uses the <a href="https://github.com/marcoroth/stimulus-parser">Stimulus Parser</a> project to statically analyze your project to figure out which controllers, targets, values, classes, etc. are available, so it can tell it when you are using undefined or unknown controllers.</p>
<p>The other benefit is that you can run the linter independently from the language server, which makes it ideal to run in CI or locally in a pre-commit hook.</p>
<p><img loading="lazy" src="/images/resized/stimulus-lint-output-263e5843-1280x.png" data-pswp-src="/images/rails-world-2025-recap/stimulus-lint-output.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>Since we are building on top of the Herb Linter architecture we also get the nice CLI and beautiful error pinpointing with syntax highlighting right in the terminal.</p>
<h2 id="turbo-lint">Turbo Lint</h2>
<p>We now have the power to better understand HTML+ERB and the Ruby inside the ERB tags, which now also allows us to build more useful tooling and a linter for Turbo.</p>
<p>We are going to do some research to see how useful this really is. If it makes sense, we are also going to publish a Turbo Lint package, specifically tailored for how you use Turbo in HTML+ERB files.</p>
<p><img loading="lazy" src="/images/resized/turbo-lint-21a0801-1280x.png" data-pswp-src="/images/rails-world-2025-recap/turbo-lint.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<h2 id="stimulus-lsp-update">Stimulus LSP Update</h2>
<p>With Herb being available now and having the Stimulus Lint extracted as it’s own package, we are going to ship an update the to Stimulus LSP to integrate these advancements.</p>
<p><img loading="lazy" src="/images/resized/stimulus-lsp-update-86e3d8dd-1280x.png" data-pswp-src="/images/rails-world-2025-recap/stimulus-lsp-update.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>This will also significantly simplify the Stimulus Language Server implementation, since most of the logic is going to live in Herb and Stimulus Lint.</p>
<p>Since we are using the Herb Parser, and the Herb Parser is able to understand Action View Tag Helpers, we are also going to get these diagnostics on Action View Tag Helpers which are very common in Rails applications.</p>
<p><img loading="lazy" src="/images/resized/stimulus-lsp-update-erb-1c9b0506-1280x.png" data-pswp-src="/images/rails-world-2025-recap/stimulus-lsp-update-erb.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>With the power of Herb, we could also introduce more advanced diagnostics, like find controller/target usages, find unused controllers, refactoring tools (like renaming), and more!</p>
<h2 id="the-rails-view-layer">The Rails View Layer</h2>
<p>ActionView was introduced at the very start of Rails in July 2004, as part of the Rails 0.5.0 release. Action View was thus the “V” in Rails’ MVC, responsible for template rendering. Back then, it was packaged within Action Pack, not as a separate gem. Even back then, it shipped with ERB (Embedded Ruby), which is still what it’s using today.</p>
<p>At <a href="/posts/railsconf-2025-recap">RailsConf</a>, I shared my vision for the Rails view layer using multiple adoption levels. Action View uses <a href="https://github.com/jeremyevans/erubi">Erubi</a> by default today, which is an ERB implementation that operates as a <strong>String Template Engine</strong>. We want to take it from being a <strong>String Template Engine</strong> to be a <strong>HTML Templating Engine</strong>. An engine that cannot produce invalid HTML and an engine that can also tell you what’s wrong about the syntax.</p>
<p>The idea is, if you give the template engine invalid HTML markup or invalid ERB syntax, it shouldn’t compile. And if you still do, it should tell you a) what is wrong, and b) where, within your template, the error is. We want to get more actionable feedback.</p>
<p>So we want to see, how we can <strong>reimagine</strong> ActionView for 2025 and beyond. We want to see how we can <strong>reengineer</strong> some of the parts of ActionView to make them more performant, easier to use, and make them more maintainable. And, we want to see if we can make ActionView <strong>reactive</strong>, so that you even need less JavaScript to build more ambitious web UIs.</p>
<p><img loading="lazy" src="/images/resized/actionview-reimagined-42c93e20-1280x.png" data-pswp-src="/images/rails-world-2025-recap/actionview-reimagined.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>So this is the initiative of <strong>ReActionView</strong>. An initiative to explore what’s possible in the Rails view layer in 2025 and beyond.</p>
<p><img loading="lazy" src="/images/resized/reactionview-90be2198-1280x.png" data-pswp-src="/images/rails-world-2025-recap/reactionview.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>The design goals are to keep <code>.html.erb</code> templates backwards-compatible (as long as you already have valid HTML markup), improve the developer experience, embrace modern web standards and new native browser features.</p>
<p>See if we can more tightly integrate modern Frontend Frameworks, and finally, to see how far we can push things in general and what we could achieve with a new wave of ideas, tooling, and libraries.</p>
<p><img loading="lazy" src="/images/resized/reactionview-design-goals-a331a8f5-1280x.png" data-pswp-src="/images/rails-world-2025-recap/reactionview-design-goals.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="ReActionView Design Goals" /></p>
<p>Over the last few years browsers really got a lot of new great features. In HTML+CSS we got Import Maps, Web Components/Custom Elements, Declarative Shadow DOM, Progressive Web Apps (PWA), and so much more.</p>
<p><img loading="lazy" src="/images/resized/html-javascript-enhancements-b3dad28c-1280x.png" data-pswp-src="/images/rails-world-2025-recap/html-javascript-enhancements.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Advancements in HTML and JavaScript" /></p>
<p>In CSS, we got CSS Grids, CSS Nesting, Container Queries, CSS <code>@function</code>’s, CSS Custom <code>@property</code>, <code>@layer</code>, and a lot more.</p>
<p><img loading="lazy" src="/images/resized/css-enhancements-8feb6a5c-1280x.png" data-pswp-src="/images/rails-world-2025-recap/css-enhancements.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Advancements in CSS" /></p>
<p>We also got a wave of new Web APIs, including the View Transitions API, Popover and Invoker Commands API, and new upcoming Speculation Rules API, which could be very interesting for the <a href="https://github.com/hotwired/turbo/pull/1427">upcoming Turbo offline mode</a>.</p>
<p><img loading="lazy" src="/images/resized/web-apis-enhancements-44ec7c6c-1280x.png" data-pswp-src="/images/rails-world-2025-recap/web-apis-enhancements.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Advancements in Web APIs" /></p>
<p>Comparing traditional Rails apps, like server-side rendering (SRR) using one of the popular HTML-over-wire frameworks, it’s quite far away from the client-side rendering (CSR) approach where you use a Rails API and one of the popular client-side frameworks to build a full Single Page Application (SPA).</p>
<p><img loading="lazy" src="/images/resized/ssr-csr-4a446f6e-1280x.png" data-pswp-src="/images/rails-world-2025-recap/ssr-csr.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Server-side rendering vs. Client-side rendering" /></p>
<p>But sadly, more and more teams are opting to use the latter because they find that HTML-over-the-wire and it’s frameworks are limiting. Teams want to build High Fidelity UIs, want to use existing Components/Libraries, might already have a lot of React Developers on staff, or have split Backend- and Frontend-Teams.</p>
<p>Hotwire is great, simple, and gets the job done in most of cases. But when you need more than what Hotwire provides you today, it’s not as easy to transition from Hotwire to a Frontend Framework. The process is not gradual. There is no onramp or migration path. You either migrate all or nothing.</p>
<p>And this is part of the reason why some teams opt to use SPAs from the beginning. Which, in my opinion, is giving away most of what makes Rails so great and productive. Let’s try to close the gap, so less and less teams actually need actually to abandon Action View. I want to see what’s possible and how far we can go. That’s why I’ve been investing in Herb to help see what we can do.</p>
<h2 id="herb-v070-and-herbengine">Herb v0.7.0 and <code>Herb::Engine</code></h2>
<p>I’m happy to announce the release of <a href="https://github.com/marcoroth/herb/releases/tag/v0.7.0">Herb v0.7.0</a>.</p>
<p><img loading="lazy" src="/images/resized/herb-v070-d616491b-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-v070.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Herb v0.7.0 Release" /></p>
<p>Herb v0.7.0 also ships with a first version of the <code>Herb::Engine</code>. The <code>Herb::Engine</code> is a new, HTML-aware ERB Rendering Engine, that’s delivering on the vision I shared at RailsConf.</p>
<p><img loading="lazy" src="/images/resized/herb-engine-ac2dfa1c-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-engine.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Herb::Engine - A new HTML-aware ERB Rendering Engine" /></p>
<p><code>Herb::Engine</code> is designed to be <code>Erubi::Engine</code> API-compatible and is meant to be a drop-in replacement for when dealing with <code>.html.erb</code> files.</p>
<h2 id="how-erubiengine-works">How <code>Erubi::Engine</code> works</h2>
<p>As I shared earlier, <code>Erubi::Engine</code> could be categorized as a <strong>String Template Engine</strong>. Which means that it doesn’t really care what code is around the <code><%</code> and <code>%></code> tags. <code>Erubi::Engine</code> uses a Regex to take your template and split it up into static parts and Ruby parts when it’s trying to compile the ERB template.</p>
<p><img loading="lazy" src="/images/resized/erubi-regex-d10e7186-1280x.png" data-pswp-src="/images/rails-world-2025-recap/erubi-regex.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Erubi: Splitting up static and dynamic parts in the template" /></p>
<p>After splitting up the template into the individual pieces, it’s calling <code>add_text()</code>, <code>add_expression()</code>, and <code>add_code()</code> to produce the compiled template.</p>
<p>The <code>add_text()</code> is being used for appending the static text parts to the compiled template, <code>add_expression()</code> for appending the String-result of the Ruby code, and <code>add_code()</code> for any other Ruby code that might be used for control flow or other logic.</p>
<p><img loading="lazy" src="/images/resized/erubi-regex-2-d7071e79-1280x.png" data-pswp-src="/images/rails-world-2025-recap/erubi-regex-2.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>In our simple example here, we get the following output.</p>
<p><img loading="lazy" src="/images/resized/erubi-compiled-output-ddb44ca6-1280x.png" data-pswp-src="/images/rails-world-2025-recap/erubi-compiled-output.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Erubi compiled Template (simplified and formatted)" /></p>
<p>We are using a <code>String</code> as a buffer to shuffle content into the buffer. At the very end we call <code>.to_s</code> to make sure we return the evaluated and rendered template as a string.</p>
<h2 id="herbengine-compared-to-erubiengine"><code>Herb::Engine</code> compared to <code>Erubi::Engine</code></h2>
<p>In contrast, the <code>Herb::Engine</code> is using the Herb Parser to first parse the template.</p>
<p><img loading="lazy" src="/images/resized/herb-engine-parse-a83e1fd0-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-engine-parse.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>In our simple example, this produces the following Syntax Tree. If we now strategically visit each of these nodes in the syntax tree we can try to compile the template by visiting all nodes.</p>
<p><img loading="lazy" src="/images/resized/herb-engine-parse-result-4550e068-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-engine-parse-result.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Herb ParseResult of the example HTML+ERB template" /></p>
<p>Since we are using Herb, we can leverage the <code>Herb::Visitor</code> which allows us to implement <code>visit_*</code> methods to strategically visit each of the node in our syntax tree.</p>
<p><img loading="lazy" src="/images/resized/herb-engine-compiler-b13043dd-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-engine-compiler.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>Let’s start by implementing the <code>HTMLOpenTagNode</code>. The <code>HTMLOpenTagNode</code> has a <code>tag_opening</code>, <code>tag_name</code>, and <code>tag_closing</code> properties. If we call <code>add_text()</code> for each of these parts we can add them to the static output of our compiled template.</p>
<p><img loading="lazy" src="/images/resized/herb-engine-html-open-tag-node-62362a3d-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-engine-html-open-tag-node.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>The <code>HTMLTextNode</code> is quite straightforward too. Since it only contains static text, we can also call <code>add_text()</code> for the <code>content</code>.</p>
<p><img loading="lazy" src="/images/resized/herb-engine-html-text-node-8ea290ac-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-engine-html-text-node.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>The <code>ERBContentNode</code> is a bit more complex, since it represents any ERB tag, like <code><%</code> or <code><%=</code>. It can check the <code>tag_opening</code> if it is a <code><%=</code>, and if it is, call <code>add_expression()</code> and otherwise call <code>add_code()</code>.</p>
<p><img loading="lazy" src="/images/resized/herb-engine-erb-content-node-7b0d5e64-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-engine-erb-content-node.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>This will call the following methods in the following order as part of visiting each node in the syntax tree.</p>
<p><img loading="lazy" src="/images/resized/herb-engine-output-172bbb73-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-engine-output.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>If we now compare this to <code>Erubi::Engine</code> you can see that this uses more method calls.</p>
<p><img loading="lazy" src="/images/resized/herb-engine-output-compared-e402e506-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-engine-output-compared.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>But since we are controlling the engine implementation we can slightly change the implementation of <code>add_text</code>, <code>add_expression</code>, and <code>add_code</code> to collect tokens, instead of directly outputting them as part of final compiled template.</p>
<p><img loading="lazy" src="/images/resized/herb-engine-collect-tokens-8978f520-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-engine-collect-tokens.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>After visiting each node in the tree, we can look at what we generated and try to compact the tokens. So if we see multiple consecutive text tokens we rewrite and combine them into a single text token.</p>
<div class="rounded-xl border">
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/rails-world-2025-recap/herb-engine-compact.png" class="" aria-label="">
<source src="/images/rails-world-2025-recap/herb-engine-compact.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6"></figcaption>
</figure>
</div>
<p>If we now call <code>add_text</code>, <code>add_expression</code>, and <code>add_code</code> for each of the tokens in the array, we get the same method calls <code>Erubi::Engine</code> produced.</p>
<p><img loading="lazy" src="/images/resized/herb-engine-output-calls-compared-a4a19451-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-engine-output-calls-compared.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>And if we compare the compiled templates you can also see that the output are identical.</p>
<p><img loading="lazy" src="/images/resized/herb-engine-compiled-template-compared-995aeb0f-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-engine-compiled-template-compared.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Erubi::Engine and Herb::Engine compiled output compared" /></p>
<p>The difference is, that <code>Herb::Engine</code> gives as all the guarantees the Herb Parser gives us. Which means we get helpful error messages and exact locations for where something went wrong in our template.</p>
<h2 id="use-herbengine-in-rails">Use <code>Herb::Engine</code> in Rails</h2>
<p>Now, let’s try to use our new ERB rendering engine in Rails. Luckily for us, <code>ActionView::Template</code> and <code>ActionView::Template::Handlers</code> makes this very easy for us to plug in our new rendering engine.</p>
<p>We can implement a new <code>Herb</code> ERB implementation within <code>ActionView::Template::Handlers::ERB</code>. Since we designed <code>Herb::Engine</code> to be API-compatible with <code>Erubi::Engine</code> we can pretty much copy the <code>Erubi</code> ERB implementation within Action View.</p>
<p><img loading="lazy" src="/images/resized/actionview-herb-erb-handler-implementation-2ebe6c6a-1280x.png" data-pswp-src="/images/rails-world-2025-recap/actionview-herb-erb-handler-implementation.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>Within the <code>ActionView::Template::Handlers::ERB</code> handler, we can autoload the <code>Herb</code> implementation and default the <code>erb_implemenation</code> to use our new <code>Herb</code> implementation.</p>
<p><img loading="lazy" src="/images/resized/actionview-erb-handler-133cae63-1280x.png" data-pswp-src="/images/rails-world-2025-recap/actionview-erb-handler.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>We can now point our app to use our Rails fork and boot our app using <code>bin/dev</code>.</p>
<p><img loading="lazy" src="/images/resized/bin-dev-f7b51292-1280x.png" data-pswp-src="/images/rails-world-2025-recap/bin-dev.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>And tada, it renders our application using the <code>Herb::Engine</code> implementation!</p>
<p><img loading="lazy" src="/images/resized/rubyevents-4085d2-1280x.png" data-pswp-src="/images/rails-world-2025-recap/rubyevents.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<h2 id="improved-error-pagesoverlays">Improved Error Pages/Overlays</h2>
<p>Since we now have the <code>Herb::Engine</code> implementation and can start to improve other things as well. Let’s start with the error page when we see a syntax error in an HTML+ERB template.</p>
<p>Currently Rails renders an error page which includes the whole template in the exception message, unformatted.</p>
<p><img loading="lazy" src="/images/resized/rails-error-page-for-view-syntax-error-e9df9528-1280x.png" data-pswp-src="/images/rails-world-2025-recap/rails-error-page-for-view-syntax-error.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>We can improve it, because the Herb Parser already exactly knows where something is wrong in the template. We already make use of this in the Herb Linter in the terminal output:</p>
<p><img loading="lazy" src="/images/resized/herb-linter-error-output-3e1d16ed-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-linter-error-output.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>So let’s bring this detail to the error page in the browser, as well:</p>
<p><img loading="lazy" src="/images/resized/herb-error-page-36c329db-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-error-page.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>This makes it so much more actionable and easier to understand what’s wrong in your template. And the beautiful thing is that you get immediate and accurate feedback in your editor, since the same parser is now responsible for powering the editor tooling and rendering the ERB in your app.</p>
<h2 id="dismissable-validation-overlays">Dismissable Validation Overlays</h2>
<p>Since we are already dealing with HTML, we can also try to add validations and report these issues. In this case we can report if you have invalid HTML, in this case nested <code><p></code> tags.</p>
<p><img loading="lazy" src="/images/resized/herb-validation-overlay-46331245-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-validation-overlay.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>We can also report if the template is trying to unsafely interpolate ERB:</p>
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/rails-world-2025-recap/herb-dimissable-validation-overlays.png" class="" aria-label="">
<source src="/images/rails-world-2025-recap/herb-dimissable-validation-overlays.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6"></figcaption>
</figure>
<p>Since these issues are not strictly “breaking” the template rendering, we report these violations as issue and make them dismissable.</p>
<h2 id="debug-view-annotations-in-development">Debug View Annotations in Development</h2>
<p>Having control over the rendering engine means we can also do more cool things. I’ve been really enjoying the <code>config.annotate_rendered_view_with_filenames</code> setting in Rails to annotate the view file names as HTML comments.</p>
<p><img loading="lazy" src="/images/resized/annotate_rendered_view_with_filenames-c988277e-1280x.png" data-pswp-src="/images/rails-world-2025-recap/annotate_rendered_view_with_filenames.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>The issue with HTML comments is that you can’t really visually style them on the rendered page in the browser.</p>
<p><img loading="lazy" src="/images/resized/annotate_rendered_view_with_filenames-output-fd23927b-1280x.png" data-pswp-src="/images/rails-world-2025-recap/annotate_rendered_view_with_filenames-output.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>But, our compiler already knows how to render HTML elements and attributes.</p>
<p>So, what if we inject additional HTML attributes, directly on the HTML elements themselves, instead of adding HTML comments with the filenames in development?</p>
<p><img loading="lazy" src="/images/resized/debug-attributes-ae0a9679-1280x.png" data-pswp-src="/images/rails-world-2025-recap/debug-attributes.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>If we render these additional debug Data Attributes and add some CSS styles:</p>
<p><img loading="lazy" src="/images/resized/debug-outlines-css-ef58f206-1280x.png" data-pswp-src="/images/rails-world-2025-recap/debug-outlines-css.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>We get really nice visual view outlines, right in the browser!</p>
<p><img loading="lazy" src="/images/resized/debug-view-outlines-27558b39-1280x.png" data-pswp-src="/images/rails-world-2025-recap/debug-view-outlines.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>If we also add another attribute for the “view type”, we can give the different types of view files different colors. So we can say views are blue, partials are green, and components are orange:</p>
<p><img loading="lazy" src="/images/resized/debug-view-type-outlines-cb23cdb0-1280x.png" data-pswp-src="/images/rails-world-2025-recap/debug-view-type-outlines.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>If we also add, the relative project path and the full path to the attributes:</p>
<p><img loading="lazy" src="/images/resized/debug-attributes-full-d84f2a24-1280x.png" data-pswp-src="/images/rails-world-2025-recap/debug-attributes-full.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>We can reveal the full project path when hovering the outline labels:</p>
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/rails-world-2025-recap/debug-hover-labels.png" class="" aria-label="">
<source src="/images/rails-world-2025-recap/debug-hover-labels.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6"></figcaption>
</figure>
<p>Another thing we can do, since we have the full path, is the open the file in the editor when clicking the outline label:</p>
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/rails-world-2025-recap/debug-label-click.png" class="" aria-label="">
<source src="/images/rails-world-2025-recap/debug-label-click.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6"></figcaption>
</figure>
<p>Since we are using CSS <code>outline</code> and HTML attributes we don’t interrupt the natural flow of the content on the page.</p>
<p>So the outlines just add the visual appearance and provide immensely helpful visual debugging. And the jump-to-source when clicking on the outline label is super helpful when you are trying to find the right file to update.</p>
<h2 id="debug-erb-output-tags-in-development">Debug ERB Output Tags in Development</h2>
<p>The next logical question is: <strong>“Can we also do this for ERB Output tags?”</strong>. And the obvious answer is, <strong>YES</strong>!</p>
<p>The Herb Debug menu has a <em>“Show ERB Output Outlines”</em> option.</p>
<p><img loading="lazy" src="/images/resized/debug-erb-output-tags-toggle-7d177eb8-1280x.png" data-pswp-src="/images/rails-world-2025-recap/debug-erb-output-tags-toggle.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>When enabled, it shows you all the dynamic parts on the current page you are on, which were rendered using a <code><%=</code> tag, which is super cool!</p>
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/rails-world-2025-recap/debug-erb-output-tags.png" class="" aria-label="">
<source src="/images/rails-world-2025-recap/debug-erb-output-tags.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6"></figcaption>
</figure>
<p>We also have an option for enabling <em>“Reveal ERB Output tag on hover”</em>.</p>
<p><img loading="lazy" src="/images/resized/debug-show-erb-on-hover-194df778-1280x.png" data-pswp-src="/images/rails-world-2025-recap/debug-show-erb-on-hover.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>When enabled, it shows the exact ERB code that was used to produce the content that is rendered on the page.</p>
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/rails-world-2025-recap/debug-erb-hover-1.png" class="" aria-label="">
<source src="/images/rails-world-2025-recap/debug-erb-hover-1.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6"></figcaption>
</figure>
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/rails-world-2025-recap/debug-erb-hover-2.png" class="" aria-label="">
<source src="/images/rails-world-2025-recap/debug-erb-hover-2.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6"></figcaption>
</figure>
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/rails-world-2025-recap/debug-erb-hover-3.png" class="" aria-label="">
<source src="/images/rails-world-2025-recap/debug-erb-hover-3.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6"></figcaption>
</figure>
<p>And the logical next step is to also implement the jump-to-source when clicking on the ERB output tags. In this case, it will open the right view file, on the right line and the right column, since the Herb Parser exactly knows where these tags were located in the original template file. Super useful!</p>
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/rails-world-2025-recap/debug-erb-click.png" class="" aria-label="">
<source src="/images/rails-world-2025-recap/debug-erb-click.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6"></figcaption>
</figure>
<h2 id="roadmap-reactionview-adoption-levels">Roadmap: ReActionView Adoption Levels</h2>
<p>So, what’s next? <strong>ReActionView</strong> is not a replacement for Action View. It’s a set of ideas and tools that remain <strong>Rails-y</strong>, aim for <strong>backwards-compatibility</strong>, and adopt modern <strong>web standards</strong>. The goal is to raise the floor of DX while keeping the on-ramp low.</p>
<p>In the original vision at RailsConf, I showed 6 Adoption Levels of how far we can take ReActionView. With this implementation we have today we reached and implemented Level 2 of that vision.</p>
<p><img loading="lazy" src="/images/resized/reactionview-adoption-levels-52a731c7-1280x.png" data-pswp-src="/images/rails-world-2025-recap/reactionview-adoption-levels.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<h3 id="level-1-and-2-try-it-locally-today">Level 1 and 2: Try it locally, today!</h3>
<p>You can try these features today in your Rails app. Add <code>reactionview</code> to your <code>Gemfile</code> and run the installer.</p>
<pre><code class="language-bash">bundle add reactionview
rails generate reactionview:install
</code></pre>
<p>This will generate the following <code>config/initializer/reactionview.rb</code> initializer:</p>
<p><img loading="lazy" src="/images/resized/reactionview-initializer-3d87f6e5-1280x.png" data-pswp-src="/images/rails-world-2025-recap/reactionview-initializer.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>You can choose to intercept all <code>.html.erb</code> files to be rendered and proccessed by <code>Herb::Engine</code> by setting <code>config.intercept_erb = true</code>:</p>
<p><img loading="lazy" src="/images/resized/reactionview-initializer-delta-193fe6d2-1280x.png" data-pswp-src="/images/rails-world-2025-recap/reactionview-initializer-delta.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>By doing so, you will get the Herb Dev Tools overlay in your Rails app:</p>
<p><img loading="lazy" src="/images/resized/herb-overlay-74690514-1280x.png" data-pswp-src="/images/rails-world-2025-recap/herb-overlay.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>Please give it a shot in your app, even if it’s just locally in your development environment and report any unexpected behavior you might encounter.</p>
<h3 id="level-3---action-view-optimizations">Level 3 - Action View Optimizations</h3>
<p>Since we have so much introspection of these HTML+ERB files, there might be some opportunities to implement more Action View Optimizations, like inlining Partial render calls, inlining Action View Tag Helpers, minifying insignificant whitespace, and probably more.</p>
<h3 id="level-4---reactive-erb-views">Level 4 - Reactive ERB Views</h3>
<p>With rendering and structural awareness in place, Herb could diff templates and re-render only what changed. In this case, we would consider all instance variables, passed from the controller to the view, as part of the “view state”.</p>
<p>If you update the state, the engine would know which part in the template is going to be affected by this state change, and would be able to only re-render the relevant part of the template, without the need to re-render the whole view.</p>
<figure>
<video width="800" height="450" controls="" loop="" muted="" poster="/images/railsconf-2025-recap/reactive-templates.png" class="" aria-label="Reactive ERB templates">
<source src="/images/railsconf-2025-recap/reactive-templates.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6">Reactive ERB templates</figcaption>
</figure>
<p>Think of this as Phoenix LiveView HEEx-like updates, but still keeping it Rails-y by using the existing <code>.html.erb</code> view files.</p>
<p>If you are not familiar with Phoenix LiveView, you can also think of this as the ERB engine emitting a set of <code><turbo-stream></code> update actions to reflect the changes in the DOM, but without the user having to explicitly tell the engine what to render and what to target using an element/ID in the DOM.</p>
<p>This would also allow for a lot of applications to be simplified and could reduce the need for Turbo Frames, Turbo Streams or Turbo Morphing in the future, since the engine itself is fully aware of the state and how it has to reflect the updates in the DOM.</p>
<p>For this to be viable we would need to find a way to serialize the state, detect state changes and then be able to broadcast these updates to the browser.</p>
<h3 id="level-5---client-side-templates">Level 5 - Client-side templates</h3>
<p>Another interesting thing to explore is similar to what React did with React Server Components (RSC). React allows certain React components to be both client-side and server-side rendered, as long as they only use a subset of the full React API.</p>
<p>We could do something similar, where we could bring certain HTML+ERB templates/partials from the server-side to the client-side and allow them to be (re-)rendered on either the server or the client as long as they only use a limited subset of ERB, essentially “ERB client components”.</p>
<p>In that case, templates would be compiled or transpiled for client-side hydration, which could allow for use cases like enabling optimistic UI updates, improved offline support, or limited interactivity without full server round-trips.</p>
<p>Ideally, the partials/components would only take in value objects using primitive types that we would be able to support in both Ruby and JavaScript. Full-blown object like ActiveRecord instances or collections wouldn’t be supported.</p>
<p>But, this is something we could detect at runtime in development and guide users on how to architect their partials/components to be fully compatible so that these templates could be used server- and client-side.</p>
<h3 id="level-6---external-components">Level 6 - External Components</h3>
<p>And finally, we explore the ability to mount external UI components (like React, Vue, Svelte, …) directly within <code>*.html.herb</code> templates. This gives users the flexibility to use the rich ecosystem of already available components in the JavaScript ecosystem without having to abandon the Rails view layer.</p>
<p>The idea is to have a initializer file, similar to how <code>importmap-rails</code> does:</p>
<p><img loading="lazy" src="/images/resized/importmap-rails-d7f25f22-1024x.png" data-pswp-src="/images/railsconf-2025-recap/importmap-rails.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="importmap-rails package mapping" /></p>
<p>In that file (like <code>config/initializers/reactionview.rb</code>) you would be able to register existing components from NPM packages, or tell it to import components from a certain directory within your app:</p>
<p><img loading="lazy" src="/images/resized/register-external-components-72ebca9c-1024x.png" data-pswp-src="/images/railsconf-2025-recap/register-external-components.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Register External Components in ReActionView" /></p>
<p>With the components registered, they will be automatically available within the context of <code>*.html.herb</code> files:</p>
<p><img loading="lazy" src="/images/resized/herb-react-85d584b6-1024x.png" data-pswp-src="/images/railsconf-2025-recap/herb-react.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Using React Components in `.herb` files" /></p>
<p>This approach is less invasive compared to <a href="https://inertia-rails.dev">Inertia.js Rails</a> which requires you to render a whole page with Inertia.</p>
<p>With this approach users are still able to use ERB next to their components in the same view template, without having to give up anything.</p>
<p>The engine would know which components are client-side and would know how to render them server-side so that can be mounted/hydrated on the client-side.</p>
<p>This approach is very similar to <a href="https://github.com/skryukov/turbo-mount">Turbo Mount</a>, with the difference that this is built right into the engine, is tightly integrated, has built-in editor tooling, requires no setup and just works out of the box.</p>
<h2 id="more-ideas-to-explore">More Ideas to Explore</h2>
<h3 id="one-off-stimulus-controllers">One-off Stimulus Controllers</h3>
<p>I heared a lot of people are somewhat frustrated with <em>“one-off”</em> Stimulus Controllers in their application. One-off Stimulus Controllers that are only really ever used in one place within the app. A lot of people have been recommending <a href="https://alpinejs.dev">Alpine.js</a> for that use case.</p>
<p>I like a lot of the ideas and advancements Alpine.js brings to the table the over Stimulus, but the idea of writing raw JavaScript inside HTML attributes somehow feels really wrong to me.</p>
<p><img loading="lazy" src="/images/resized/alpinejs-example-2fdd24ad-1280x.png" data-pswp-src="/images/rails-world-2025-recap/alpinejs-example.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>There might be some more opportunities to create some kind of “Inline Stimulus” version too. I really like the way Svelte handles this.</p>
<p><img loading="lazy" src="/images/resized/svelte-7b33bff2-1280x.png" data-pswp-src="/images/rails-world-2025-recap/svelte.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>Instead of having the Stimulus Controller in it’s own <code>*_controller.js</code> file, we could think of way to have a <code><script></code> tag within the file that’s only being transformed/applied to the current file you are in. So you could write some custom one-off JavaScript that’s only being used and available/accessible in this one file you are writing the code in.</p>
<h3 id="view-file-scoped-css">View File Scoped CSS</h3>
<p>We could do something very similar to CSS and <code><style></code> tags as well. This is largely inspired by Vue.js, that allows you to use a special <code><style scoped></code> tag, that gets transformed when the template gets compiled.</p>
<p>The idea is that you can write a <code><style scoped></code> tag, and all of the CSS rules you are declaring are being scoped to that single file.</p>
<p><img loading="lazy" src="/images/resized/vuejs-style-scoped-a4ad71cc-1280x.png" data-pswp-src="/images/rails-world-2025-recap/vuejs-style-scoped.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>In this case, the <code><style scoped></code> gets transformed to a regular <code><style></code> tag and gets scoped with a special selector that gets inserted at compile time.</p>
<p><img loading="lazy" src="/images/resized/vuejs-style-scoped-transformed-f0af1115-1280x.png" data-pswp-src="/images/rails-world-2025-recap/vuejs-style-scoped-transformed.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>In this case, it’s adding a new <code>data-*</code> attribute to both all CSS rules within the <code><style></code> tag, and the data attribute to the HTML element itself that’s being used to scope the style. With that, we make sure that none of these styles are leaking to other parts of the application, which is quite clever.</p>
<h3 id="selective-rendering">Selective Rendering</h3>
<p>And the final idea is <em>“Selective Rendering”</em>. This might come in super handy when paired with concepts like Turbo Frames. Let’s say you have the following view file:</p>
<p><img loading="lazy" src="/images/resized/selective-rendering-template-6c68b235-1280x.png" data-pswp-src="/images/rails-world-2025-recap/selective-rendering-template.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>Instead of rendering the whole view, we can pass the partial an argument to look for a certain selector.</p>
<p><img loading="lazy" src="/images/resized/selective-rendering-render-call-f07e376d-1280x.png" data-pswp-src="/images/rails-world-2025-recap/selective-rendering-render-call.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>Since Herb knows about the structure, hierarchy, the HTML elements and it’s attributes we could filter by a certain selector you pass it, and precompile a subsection of that template.</p>
<p>In the case of the Turbo Frame and the <code>#reviews</code> selector we could prepare and precompile only the following subset of of the bigger template.</p>
<p><img loading="lazy" src="/images/resized/selective-rendering-template-subsection-fa5dba82-1280x.png" data-pswp-src="/images/rails-world-2025-recap/selective-rendering-template-subsection.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="" /></p>
<p>This would allow to optimize render performance, since you only ever render what you actually need, which in the case of Turbo Frames is usually only the Turbo Frame itself.</p>
<p>The good thing is that we could also tell that a certain selector wouldn’t match anything in the given template, since we can statically analyze and search for these selectors within the template you are trying to precompile and render.</p>
<hr />
<h2 id="why-this-direction">Why this direction</h2>
<p>Action View and ERB are here to stay. Rails remains a full-stack framework built around server-rendered HTML. If we want that story to continue to scale, we need <strong>first-class HTML tooling</strong> and a <strong>clear on-ramp</strong> to richer UIs when needed, without forcing apps to abandon ERB.</p>
<p>The idea is to bring more of these modern frontend features to Rails so that there is less of a need to abandon the Rails way. Rails shouldn’t discourage the use of (modern) JavaScript. Rails should embrace it. Rails should also embrace modern web standards and the exciting new APIs and features browsers ship with today.</p>
<p>Rails should provide good defaults and built-in options/migration paths when you need more. This is what Rails was always known for. I believe that the view layer needs to scale in the same way as everything else in the Rails framework does.</p>
<p>But it’s important to me to say that this is a vision. If we, as a framework and community, want to stay relevant we need to explore what’s possible. New innovations require exploration.</p>
<p>Even if none of this is viable in production it’s still worth exploring. The Herb tooling that came out of this is so valuable already, and I also believe that there’s more to achieve with the ReActionView initiative.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Prism had a big effect on Ruby internals and the tooling landscape in general. Prism now ships with Ruby 3.4+ as the default parser. And I believe Herb already had a similar effect for HTML Templating and Tooling.</p>
<p>Herb started as a Parser and is now an ecosystem of valuable tools that Rails developers are using on a daily basis. Meanwhile, ReActionView started as a vision to address some of the shortcomings in the Rails View Layer. Today it might be just an alternative ERB Rendering engine, but I believe that this already shows what could be possible.</p>
<p>We can push more boundaries and bring the whole Rails Framework forward so that it can hold up with the current JavaScript stacks. Also so that teams don’t have to abandon the beauty of Action View.</p>
<p>The community has built incredible tooling for Ruby itself. Now it’s time to bring that same care to the view layer. With Herb and ReActionView, I believe, we have a path to level up the view layer and make Rails’ story awesome for the frontend.</p>
<p>Rails has a unique position as a full stack framework and I want to keep it that way. I want to keep Rails as attractive and competitive as possible for both people that are new and people that have been using Rails for a long time.</p>
<hr />
<h2 id="thanks--links">Thanks & links</h2>
<p>I’m incredibly grateful for all the support, feedback, encouragement, and motivation I got over the span of 2025.</p>
<div class="grid grid-cols-2 gap-2">
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/rails-world-1-bfc940bc-256x.png" data-pswp-src="/images/rails-world-2025-recap/rails-world-1.png" data-pswp-width="6012" data-pswp-height="4008" class="rounded-md object-cover w-full h-full" alt="" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/rails-world-2-99ff2be2-256x.png" data-pswp-src="/images/rails-world-2025-recap/rails-world-2.png" data-pswp-width="6012" data-pswp-height="4008" class="rounded-md object-cover w-full h-full" alt="" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/rails-world-3-26e53622-256x.png" data-pswp-src="/images/rails-world-2025-recap/rails-world-3.png" data-pswp-width="6012" data-pswp-height="4008" class="rounded-md object-cover w-full h-full" alt="" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/rails-world-4-3233831c-256x.png" data-pswp-src="/images/rails-world-2025-recap/rails-world-4.png" data-pswp-width="6012" data-pswp-height="4008" class="rounded-md object-cover w-full h-full" alt="" />
</div>
</div>
<p>Thank you to everyone who came to my talk, shared ideas/feedback, or took the time to chat in Amsterdam. ❤️</p>
<p>If you try the tools and hit issue, please feel free to open an issue. The feedback loop is what makes this really useful.</p>
<p><img loading="lazy" src="/images/resized/summary-91b66a01-1280x.png" data-pswp-src="/images/rails-world-2025-recap/summary.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Rails World 2025 Summary Slide" /></p>
<p>I’m super excited about the future of the Rails view layer, and would love to see how far we can take it!</p>
<p>Thank you!</p>
<p>— Marco</p>
<hr />
<ul>
<li>Slides: <a href="https://speakerdeck.com/marcoroth/introducing-reactionview-a-new-actionview-compatible-erb-engine-at-rails-world-2025-amsterdam">Speaker Deck</a></li>
<li>ReActionView: <a href="https://github.com/marcoroth/reactionview">https://github.com/marcoroth/reactionview</a></li>
<li>Herb: <a href="https://github.com/marcoroth/herb">https://github.com/marcoroth/herb</a></li>
<li>GitHub Sponsors: <a href="https://github.com/sponsors/marcoroth">https://github.com/sponsors/marcoroth</a></li>
</ul>Marco RothIntroducing the Herb Linter, Formatter, and a Vision for the Future of Rails Views2025-07-17T09:00:00+00:002025-07-17T09:00:00+00:00repo://posts.collection/_posts/2025-07-17-railsconf-2025-recap.md<p>The final RailsConf just wrapped up in Philadelphia last week. Here’s a recap of my talk, what we launched, and where we’re going next.</p>
<p>RailsConf this year was special for me, not just because it was an honor to be part of the final edition of such an important event in the Ruby community, but also because I got to share a new release and vision for the framework I’ve used most of my career.</p>
<p>This blog post is a summary and recap of my RailsConf 2025 talk titled “The Modern View Layer Rails Deserves: A Vision for 2025 and Beyond”.</p>
<h2 id="herb-v040-linter-formatter-preview-and-a-big-step-forward">Herb v0.4.0: Linter, Formatter (Preview), and a Big Step Forward</h2>
<p>The first headline from the talk is the release of <a href="https://github.com/marcoroth/herb/releases/tag/v0.4.0"><strong>Herb v0.4.0</strong></a>, and with it, the first versions of the <a href="https://herb-tools.dev/projects/linter"><strong>Herb Linter</strong></a> and a first <em>preview version</em> of the <a href="https://herb-tools.dev/projects/formatter"><strong>Herb Formatter</strong></a>.</p>
<p><img loading="lazy" src="/images/resized/herb-v040-5dec4a64-1024x.png" data-pswp-src="/images/railsconf-2025-recap/herb-v040.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Herb Release v0.4.0" /></p>
<p>The most notable things in the release are:</p>
<ul>
<li>Herb Linter</li>
<li>Herb Formatter (Preview)</li>
<li>Improved Language Server</li>
<li>Improved Visual Studio Code Extension</li>
<li>Parser Bug Fixes and compatibility improvements</li>
</ul>
<p>You can check the <a href="https://github.com/marcoroth/herb/releases/tag/v0.4.0"><strong>full Herb v0.4.0 changelog</strong></a> on GitHub.</p>
<p>These new tools are all built on top of the <a href="/posts/introducing-herb">Herb Parser</a>, a new fault-tolerant, HTML-aware ERB parser written in C, which I introduced at <a href="/posts/introducing-herb">RubyKaigi</a> earlier this year. These tools aim to bring the modern developer experience we expect to <code>*.html.erb</code> files.</p>
<p>But more importantly, they’re fully integrated into the <a href="https://herb-tools.dev/projects/language-server">Herb Language Server</a> and the improved Visual Studio Code extension, which now:</p>
<ul>
<li>supports linting and auto-formatting directly in the editor</li>
<li>offers whole-project analysis</li>
<li>includes a new sidebar panel for diagnostics and the analysis results</li>
</ul>
<p>You can use the linter and formatter via the CLI or in-editor.</p>
<p><img loading="lazy" src="/images/resized/vscode-e6094615-1024x.png" data-pswp-src="/images/railsconf-2025-recap/vscode.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Herb Visual Studio Code Extension" /></p>
<h3 id="herb-linter">Herb Linter</h3>
<p>The <a href="https://rubystyle.guide">Ruby Style Guide</a> provides recommendations and best practices for Ruby code. Linters like <a href="https://rubocop.org">RuboCop</a> and <a href="https://github.com/standardrb/standard">Standard</a> make this actionable by implementing and enforcing these guidelines.</p>
<p>But for view files, we haven’t had many resources or style guides on writing good views. The Herb Linter helps guide developers towards best practices and avoid common mistakes in HTML+ERB files.</p>
<h4 id="cli">CLI</h4>
<p>You can run the linter using the CLI and get nicely annotated and syntax highlighted snippets for the offenses the linter found right in the console.</p>
<p><img loading="lazy" src="/images/resized/linter-cli-offense-ac43e6d1-1024x.png" data-pswp-src="/images/railsconf-2025-recap/linter-cli-offense.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Offense in CLI output" /></p>
<p>At the end of the run, you’ll get a summary of the most violated rules:</p>
<p><img loading="lazy" src="/images/resized/linter-cli-summary-dea1733b-1024x.png" data-pswp-src="/images/railsconf-2025-recap/linter-cli-summary.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Herb Lint CLI Summary" /></p>
<p>For lots of offenses, use the <code>--simple</code> flag for more compact output:</p>
<p><img loading="lazy" src="/images/resized/linter-cli-simple-cfe2967e-1024x.png" data-pswp-src="/images/railsconf-2025-recap/linter-cli-simple.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Herb Linter Simple CLI output" /></p>
<h4 id="language-server-integration">Language Server Integration</h4>
<p>While a good CLI is important, catching offenses directly in your editor provides an even tighter feedback loop:</p>
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/railsconf-2025-recap/linter.png" class="" aria-label="Herb Linter Language Server Integration">
<source src="/images/railsconf-2025-recap/linter.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6">Herb Linter Language Server Integration</figcaption>
</figure>
<p>All the Linter rule offenses have a clickable Rule ID in their diagnostic popup you can click on to learn why this rule exists and why it’s important:</p>
<p><img loading="lazy" src="/images/resized/linter-rule-id-4f375e2c-1024x.png" data-pswp-src="/images/railsconf-2025-recap/linter-rule-id.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Linter Rule diagnostic popup" /></p>
<p>Clicking on a rule ID will open the documentation for that specific linter rule:</p>
<p><img loading="lazy" src="/images/resized/linter-rule-documentation-1d8e2df0-1024x.png" data-pswp-src="/images/railsconf-2025-recap/linter-rule-documentation.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Linter Rule Documentation" /></p>
<p>At the end of each linter rule documentation page you will find a few examples of how good and bad code looks like according to the rule:</p>
<p><img loading="lazy" src="/images/resized/linter-rule-examples-f31d4ace-1024x.png" data-pswp-src="/images/railsconf-2025-recap/linter-rule-examples.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Linter Rule Good/Bad examples" /></p>
<p>We also run the Linter over all code blocks in the documentation and annotate the examples using <a href="https://shiki.style">Shiki</a> and <a href="https://twoslash.netlify.app">Twoslash</a>.</p>
<p>This really helps to understand what’s wrong about the given examples. I hope that this makes the rules easier to understand, easier to reason about, and more actionable since you can see exactly where the violation is and can see the actual linter message right in the example.</p>
<h4 id="more-linter-rules-and-feedback">More Linter Rules and Feedback</h4>
<p>You can check out the full list of already implemented linter rules in the <a href="https://herb-tools.dev/linter/rules/">documentation</a>. There are also a <a href="https://github.com/marcoroth/herb/issues?q=is%3Aissue%20state%3Aopen%20label%3Alinter-rule">lot more rules to be implemented</a> if you are looking for a way to contribute to the project.</p>
<p>Please open an issue if you can think of any rule the Herb Linter should support. I would also be very thankful if you could report any false-positives the Linter found in your code.</p>
<p><img loading="lazy" src="/images/resized/report-quick-fix-cbcba214-720x.png" data-pswp-src="/images/railsconf-2025-recap/report-quick-fix.png" data-pswp-width="810" data-pswp-height="198" class="rounded-xl border" alt="Linter Report Quick Fix" /></p>
<p>You can use the quick fix action to report an error, it will open a new GitHub issue and pre-fill some of the metadata automatically for you.</p>
<h3 id="herb-formatter-preview">Herb Formatter (Preview)</h3>
<p>Alongside the linter I also wanted to release a preview version of the Herb Formatter so people can try it themselves on their own templates, with the hope that we can catch bugs and edge cases the formatter doesn’t handle well yet.</p>
<p>Please, feel free to report any case the formatter formats the document in a weird or unpredictable way.</p>
<h4 id="format-on-save">Format on Save</h4>
<p>The Herb Formatter is also fully integrated into the <a href="https://herb-tools.dev/projects/language-server">Herb Language Server</a> but is disabled by default for the time being.</p>
<p>You can enable the Formatter in Visual Studio Code in the settings, but please make sure to only run it over documents you can revert/restore:</p>
<p><img loading="lazy" src="/images/resized/herb-formatter-vscode-eb73f5a-1024x.png" data-pswp-src="/images/railsconf-2025-recap/herb-formatter-vscode.png" data-pswp-width="2228" data-pswp-height="1122" class="rounded-xl border" alt="Herb Formatter Setting in Visual Studio Code" /></p>
<p>Once enabled it will format your files on save as long as there is no error present in the document:</p>
<figure>
<video width="800" height="450" controls="" autoplay="" loop="" muted="" poster="/images/railsconf-2025-recap/formatter-poster.png" class="" aria-label="Visual Studio Code Herb Format on Save">
<source src="/images/railsconf-2025-recap/formatter.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6">Visual Studio Code Herb Format on Save</figcaption>
</figure>
<p>Seeing the formatter coming to life is a very satisfying feeling, as this has been something I wanted to have for the longest time. Now this dream is slowly turning into reality and I’m super excited to see it evolve!</p>
<p>If you see the formatter behaving in an unexpected way or tripping over something, please open an issue! I’d love to get this right so that we can settle on a good default for the community.</p>
<h2 id="a-vision-for-the-rails-view-layer">A Vision For The Rails View Layer</h2>
<h3 id="context">Context</h3>
<p>Action View has been part of Rails since it initially released in July 2004 with Rails 0.5.0.</p>
<blockquote>
<p>Rails is an open source web-application framework for Ruby. It ships with an answer for every letter in MVC: Action Pack for the Controller and View, Active Record for the Model. <a href="https://rubytalk.org/t/ann-rails-0-5-0-the-end-of-vaporware/12744"><em>From the original announcement</em></a></p>
</blockquote>
<p>ERB (Embedded Ruby) has been the default since the first release and still is today.</p>
<p>Here are some of the big changes in Action View over the last 20 years:</p>
<ul>
<li>2005 – Rails 1.0 – Layouts + Partials</li>
<li>2007 – Rails 2.0 – <code>*.html.erb</code> (<code>show.html.erb</code> instead of <code>show.rhtml</code>)</li>
<li>2007 – Rails 2.3 – <a href="https://github.com/rails/render_component"><code>render_component</code></a> deprecated</li>
<li>
<p>2010 – Rails 3.0 – Automatic HTML escaping</p>
<p>Auto-Escaping and SafeBuffer: One of the biggest conceptual changes in Rails 3 was the introduction of automatic HTML escaping by default in templates. Prior to Rails 3, developers had to explicitly call the <code>h</code> helper to escape user-provided content.</p>
</li>
<li>
<p>2010 – Rails 3.0 – <code>ActionView::Template</code></p>
<p>The Rails-Merb merge brought some internal improvements to ActionView. Templates were now handled by an <code>ActionView::Template</code> abstraction with a unified interface for different handlers like ERB, Builder, etc.</p>
</li>
<li>
<p>2013 – Rails 4.0 – Russian Doll Caching</p>
<p>Tracks template dependencies with digest keys</p>
</li>
<li>
<p>2014 – Rails 4.1 – Action View Variants</p>
<p><code>actionview</code> is extracted as it’s own gem</p>
</li>
<li>2017 – Rails 5.1 – <code>form_with</code></li>
<li>
<p>2020 – Rails 6.1 (Beta) – <a href="https://github.com/rails/rails/pull/36388"><code>ActionView::Component</code></a></p>
<p>While <code>ActionView::Component</code> didn’t make it into Rails 6.1 but eventually became <a href="https://viewcomponent.org">ViewComponent</a>.</p>
</li>
<li>
<p>2020 – Rails 6.1 – <code>render_in(context)</code></p>
<p>Even though <code>ActionView::Component</code> didn’t make it into Rails we got the new <code>render_in</code> API that allows pretty much any Ruby object to be rendered by the Rails <code>render</code> method as long as it implements the <code>render_in</code> interface.</p>
</li>
<li>2021 – Rails 7.0 – Hotwire by default</li>
<li>2023 – Rails 7.1 – Partial Strict Locals</li>
</ul>
<hr />
<p>Even though Action View has seen a lot of improvements and optimizations over the years, I think it’s fair to say that the public API of Action View hasn’t really changed a lot over all these years.</p>
<p>Most of the features/changes were additive and not disruptive, which is great, since that makes the API really stable and easy to rely/build on.</p>
<p><img loading="lazy" src="/images/resized/rails-view-layer-ecosystem-dca97cee-1024x.png" data-pswp-src="/images/railsconf-2025-recap/rails-view-layer-ecosystem.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Tools in the Rails View Layer" /></p>
<p>Even though we have seen a lot of tools, approaches, templating engines and syntaxes come and go over these years, Action View and ERB are still going strong and aren’t going anywhere. They are also going to remain the default in Rails for the foreseeable future.</p>
<p>Rails always has been about server-side rendered HTML and is going to stay with that too. In the end, Action View is really what makes Rails a true full-stack framework. But this also means that advanced HTML tooling is only getting more important if we want to keep up with the developer tooling in the greater web ecosystem.</p>
<p>But this server-side rendered approach doesn’t come without its issues:</p>
<p><img loading="lazy" src="/images/resized/actionview-issues-49844075-1024x.png" data-pswp-src="/images/railsconf-2025-recap/actionview-issues.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Action View Issues" /></p>
<p>Some of these problems are solved with tools like <a href="https://viewcomponent.org">ViewComponent</a>, <a href="https://github.com/bullet-train-co/nice_partials">nice_partials</a> or other similar tools, but there’s still a lot left to be solved, especially in the developer tooling space.</p>
<p>But what if we could solve these problems at a deeper level? What if we could improve not just the tooling, but the rendering engine itself?</p>
<h3 id="a-new-erb-rendering-engine">A new ERB Rendering Engine</h3>
<p>The Herb project started with a simple goal: build better HTML+ERB tooling (formatters, linters, language servers). There was no plan to build a new ERB rendering engine.</p>
<p><img loading="lazy" src="/images/resized/erb-rendering-engine-fd029e88-1024x.png" data-pswp-src="/images/railsconf-2025-recap/erb-rendering-engine.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Vision for a new ERB Rendering Engine" /></p>
<p>But the more and more I worked on fulfilling the tooling vision I started to realize that there also may be an opportunity to improve the way we render HTML+ERB templates.</p>
<p>Currently all ERB rendering engines treat the non-Ruby code as Strings, making them a <strong>String Templating Language</strong>. But what we want, ideally, is an <strong>HTML Templating Language</strong>: an engine that cannot produce invalid HTML, because the engine is aware of the HTML structure around the ERB tags in the template.</p>
<p>And that’s when I started to think about this a bit more, and what I came up with is a vision for an ActionView-compatible framework for Rails, code-named “ReActionView”:</p>
<h2 id="reactionview">ReActionView</h2>
<p>I structured the ReActionView vision into 6 adoption levels, the more we implement, the more advanced it gets. And we certainly don’t have to implement all levels for this to be useful.</p>
<p><img loading="lazy" src="/images/resized/reactionview-9b8f94be-1024x.png" data-pswp-src="/images/railsconf-2025-recap/reactionview.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="ReActionView Logo" /></p>
<p><strong>It’s important for me to note</strong> that this is a vision and that new advancements require exploration. Even if none of this is viable in production it’s still worth exploring to see how far we can go, how far we can push things, or discover other patterns by taking some ideas to an extreme, even if they are not viable.</p>
<p>But with that out of the way, let me walk you through the six adoption levels.</p>
<h3 id="level-1-better-feedback-and-developer-experience">Level 1: Better Feedback and Developer Experience</h3>
<p>One of the nice features I always liked in Rails is the interactive exception screen, that would show you all sorts of helpful context like backtraces, environment variables, the failing line and surrounding lines, and most importantly the interactive debugging console.</p>
<p><img loading="lazy" src="/images/resized/rails-exception-screen-44506bca-1024x.png" data-pswp-src="/images/railsconf-2025-recap/rails-exception-screen.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Rails Exception Screen" /></p>
<p>This works great if the exception happens in an <code>.rb</code> file. Sadly it doesn’t work as nicely if you have syntax errors in <code>*.html.erb</code> files.</p>
<p><img loading="lazy" src="/images/resized/rails-exception-screen-views-e9df9528-1024x.png" data-pswp-src="/images/railsconf-2025-recap/rails-exception-screen-views.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Rails exception screen" /></p>
<p>And I think that’s something Herb can help with and improve. A lot of the Herb tooling is inspired by Vite and it’s great ecosystem and of tools around it.</p>
<p><img loading="lazy" src="/images/resized/vite-error-f096bfcc-1024x.png" data-pswp-src="/images/railsconf-2025-recap/vite-error.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Vite HTML exception screen" /></p>
<p>In Vite, if you introduce an HTML error, it will instantly show you this error message on the screen, even without having to reload the page.</p>
<p>Since Herb can already tell if something is wrong/incorrect about a HTML+ERB template we can improve the exception screen to show what and where something is wrong in the template.</p>
<p><img loading="lazy" src="/images/resized/reactionview-html-error-32b431e8-1024x.png" data-pswp-src="/images/railsconf-2025-recap/reactionview-html-error.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="ReActionView HTML exception screen" /></p>
<p>On this level Herb acts as a parser and analyzer. It catches common issues in real time.</p>
<h3 id="level-2-html-aware-erb-rendering-engine">Level 2: HTML-aware ERB rendering Engine</h3>
<p>On this level, we would take the ERB rendering engine from being a <strong>String Template Engine</strong> to an actual <strong>HTML Template Engine</strong>. The idea is to keep everything backwards compatible, as long as the current <code>*.html.erb</code> view files already contain and render valid HTML.</p>
<p>Instead of using ERB’s default engine, templates could be rendered using a new Herb rendering engine.</p>
<p>Part of the inspiration of ReActionView and the actual project name “Herb” comes from Elixir and their way of approaching templating languages.</p>
<p>Elixir has an ERB equivalent called <code>EEx</code>, which is very similar and has almost identical syntax. The interesting piece is that Phoenix LiveView/Elixir also has a more specific version of EEx which combines EEx with HTML, called HEEx.</p>
<p><img loading="lazy" src="/images/resized/ruby-elixir-52e265d5-1024x.png" data-pswp-src="/images/railsconf-2025-recap/ruby-elixir.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Ruby/Elixir rendering engines" /></p>
<p>Looking at this, we don’t really have an equivalent in the Ruby ecosystem that combines HTML with ERB in a rendering engine. And this is where the name Herb comes from, the <strong>H</strong> from HTML and the ERB, following the naming scheme of HEEx.</p>
<p>Let’s look at an example how a <strong>String Template Engine</strong> would compare to a <strong>HTML Template Engine</strong>. Given the following ERB template (which has a missing <code></h1></code> closing tag):</p>
<pre><code class="language-erb"><h1>Hello, <%= name %>!
</code></pre>
<p>and ask Action View to compile it using:</p>
<pre><code class="language-ruby">template = ActionView::Template.new(
"<h1>Hello, <%= name %>!",
"test.html.erb",
ActionView::Template::Handlers::ERB,
locals: [:name]
)
compiled = template.handler.call(
template,
template.source
)
</code></pre>
<p>it will happily compile the template. Action View is only concerned about turning the given template into compiled Ruby code that concatenates strings:</p>
<p><img loading="lazy" src="/images/resized/actionview-erb-compile-84e2e165-1024x.png" data-pswp-src="/images/railsconf-2025-recap/actionview-erb-compile.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Compile ERB with ActionView" /></p>
<p>Whereas, if we would build a new <code>.herb</code> HTML+ERB (Herb) engine, that would care about the surrounding HTML in an ERB template and we tried to compile it using Action View, it would not compile and tell you what’s wrong with the given template.</p>
<p><img loading="lazy" src="/images/resized/actionview-herb-compile-21e0f9fa-1024x.png" data-pswp-src="/images/railsconf-2025-recap/actionview-herb-compile.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Compile Herb with ActionView" /></p>
<p>This way, we can guarantee that the rendering engine cannot even produce invalid HTML and would just raise if Action View tried to compile an invalid template.</p>
<p>Plus we get the benefit of the rendering engine directly telling us what and where something is wrong about the template we tried to compile, which makes the whole developer experience a lot nicer and more predictable, instead of it silently failing.</p>
<h3 id="level-3-action-view-optimizations">Level 3: Action View Optimizations</h3>
<p>Given that we have much more introspection and structural awareness of HTML+ERB files now, there might be more opportunities to do lower-level optimizations for when Action View is trying to compile a template.</p>
<p>John Hawthorn did some similar work on <a href="https://github.com/jhawthorn/actionview_precompiler"><code>actionview_precompiler</code></a> (and <a href="https://github.com/jhawthorn/dynamic_locals"><code>dynamic_locals</code></a>) that is very interesting and might be something we could use as a starting point or improve upon.</p>
<p>Another possible optimization: inlining <code><%= render partial: "post" %></code> calls. This would eliminate runtime partial lookups by directly embedding the partial content into the compiled view.</p>
<p>Instead of compiling to <code>@output_buffer.append=( render partial: 'post' );</code>, we would read and inline the partial at compile time.</p>
<p>This requires confidence in identifying the right partial, which is why our <a href="https://github.com/marcoroth/herb/issues/160">linter rules</a> encourage explicit render arguments. Worst case: we fall back to current behavior. Best case: we eliminate runtime lookups.</p>
<p>I have to be 100% honest here, I haven’t really looked into how feasible these kind of ideas/optimizations are in practice, but it’s hard to imagine that we wouldn’t be able to get something out of this.</p>
<p>If you have any ideas or insights here, please reach out and let me know! I’d love to chat and hear what we could do or how we could approach this.</p>
<h3 id="level-4-reactive-erb-templates">Level 4: Reactive ERB Templates</h3>
<p>With rendering and structural awareness in place, Herb could diff templates and re-render only what changed. In this case, we would consider all instance variables, passed from the controller to the view, as part of the “view state”.</p>
<p>If you update the state, the engine would know which part in the template is going to be affected by this state change, and would be able to only re-render the relevant part of the template, without the need to re-render the whole view.</p>
<figure>
<video width="800" height="450" controls="" loop="" muted="" poster="/images/railsconf-2025-recap/reactive-templates.png" class="" aria-label="Reactive ERB templates">
<source src="/images/railsconf-2025-recap/reactive-templates.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<figcaption class="text-center text-sm -mt-6">Reactive ERB templates</figcaption>
</figure>
<p>Think of this as Phoenix LiveView HEEx-like updates, but still keeping it Rails-y by using the existing <code>.html.erb</code> view files.</p>
<p>If you are not familiar with Phoenix LiveView, you can also think of this as the ERB engine emitting a set of <code><turbo-stream></code> update actions to reflect the changes in the DOM, but without the user having to explicitly tell the engine what to render and what to target using an element/ID in the DOM.</p>
<p>This would also allow for a lot of applications to be simplified and could soften the need for Turbo Frames, Turbo Streams or Turbo Morphing in the future, since the engine itself is fully aware of the state and how it has to reflect the updates in the DOM.</p>
<p>For this to be viable we would need to find a way to serialize the state, detect state changes and then be able to broadcast these updates to the browser.</p>
<h4 id="opt-in-syntax-and-strict-mode">Opt-In Syntax and Strict Mode</h4>
<p>While we could make these reactive ERB templates work with regular <code>*.html.erb</code> files, there is also an opportunity to introduce a new <code>*.html.herb</code> file-ending that would have this behavior turned on by default.</p>
<p><img loading="lazy" src="/images/resized/erb-herb-file-rename-e3c06c9f-1024x.png" data-pswp-src="/images/railsconf-2025-recap/erb-herb-file-rename.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Rename the file extension to opt-in to new behavior" /></p>
<p>This opt-in approach allows users to gradually adopt this new style of writing views, while keeping existing view files compatible. For views that don’t require any advanced interaction you would just keep the file-ending as <code>*.html.erb</code>. You would only use and rename existing views to <code>*.html.herb</code> where it’s needed and makes sense.</p>
<p>The philosophy of progressive enhancement is really valuable here and doesn’t require you to follow an “All or nothing” approach when migrating/adopting this new approach.</p>
<p>Adopting a new file-ending would technically also allow for new syntax to be introduced. I’m not saying we need to introduce new syntax, but being able to render (view) components using a capitalized first character (similar to <a href="https://github.com/camertron/rux">Rux</a>/JSX) could prove to become very handy.</p>
<pre><code class="language-ruby">class NameComponent < ApplicationComponent
def initialize(first_name:, last_name:)
@first_name = first_name
@last_name = last_name
end
end
</code></pre>
<p>and render it using:</p>
<pre><code class="language-jsx"><NameComponent first-name="Rails" last-name="Conf" />
</code></pre>
<p>Another thing we could enforce using a new file-ending with stricter rules would be things like:</p>
<ul>
<li>Check for invalid syntax</li>
<li>Check for valid HTML5 (via Nokogiri)</li>
<li>Accessibility/A11y Checks (i.e. missing <code>alt</code> attributes on <code><img></code> tags)</li>
<li>Cross-site scripting/XSS Checks (i.e no unsafe ERB interpolation)</li>
<li>Only one element with the same ID</li>
<li>…</li>
</ul>
<p><img loading="lazy" src="/images/resized/reactionview-errors-5aacca7f-1024x.png" data-pswp-src="/images/railsconf-2025-recap/reactionview-errors.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="ReActionView showing validation errors in development" /></p>
<p>These checks could fail the compilation step, making sure that the templates adhere to a certain level of quality, safety, semantic correctness, and compliance with modern web standards and best practices before they ever reach production.</p>
<h3 id="level-5-universal-client-side-templates">Level 5: Universal client-side Templates</h3>
<p>Another interesting thing to explore is similar to what React did with React Server Components (RSC). React allows certain React components to be both client-side and server-side rendered, as long as they only use a subset of the full React API.</p>
<p>We could do something similar, where we could bring certain HTML+ERB templates/partials from the server-side to the client-side and allow them to be (re-)rendered on either the server or the client as long as they only use a limited subset of ERB, essentially “ERB client components”.</p>
<p>In that case, templates would be compiled or transpiled for client-side hydration, which could allow for use cases like enabling optimistic UI updates, improved offline support, or limited interactivity without full server round-trips.</p>
<p>Ideally, the partials/components would only take in value objects using primitive types that we would be able to support in both Ruby and JavaScript. Full-blown object like ActiveRecord instances or collections wouldn’t be supported.</p>
<p>But, this is something we could detect at runtime in development and guide users on how to architect their partials/components to be fully compatible so that these templates could be used server- and client-side.</p>
<h3 id="level-6-external-components">Level 6: External Components</h3>
<p>And finally, we explore the ability to mount external UI components (like React, Vue, Svelte, …) directly within <code>*.html.herb</code> templates. This gives users the flexibility to use the rich ecosystem of already available components in the JavaScript ecosystem without having to abandon the Rails view layer.</p>
<p>The idea is to have a initializer file, similar to how <code>importmap-rails</code> does:</p>
<p><img loading="lazy" src="/images/resized/importmap-rails-d7f25f22-1024x.png" data-pswp-src="/images/railsconf-2025-recap/importmap-rails.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="importmap-rails package mapping" /></p>
<p>In that file (like <code>config/initializers/reactionview.rb</code>) you would be able to register existing components from NPM packages, or tell it to import components from a certain directory within your app:</p>
<p><img loading="lazy" src="/images/resized/register-external-components-72ebca9c-1024x.png" data-pswp-src="/images/railsconf-2025-recap/register-external-components.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Register External Components in ReActionView" /></p>
<p>With the components registered, they will be automatically available within the context of <code>*.html.herb</code> files:</p>
<p><img loading="lazy" src="/images/resized/herb-react-85d584b6-1024x.png" data-pswp-src="/images/railsconf-2025-recap/herb-react.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="Using React Components in `.herb` files" /></p>
<p>This approach is less invasive compared to <a href="https://inertia-rails.dev">Inertia.js Rails</a> which requires you to render a whole page with Inertia.</p>
<p>With this approach users are still able to use ERB next to their components in the same view template, without having to give up anything.</p>
<p>The engine would know which components are client-side and would know how to render them server-side so that can be mounted/hydrated on the client-side.</p>
<p>This approach is very similar to <a href="https://github.com/skryukov/turbo-mount">Turbo Mount</a>, with the difference that this is built right into the engine, is tightly integrated, has built-in editor tooling, requires no setup and just works out of the box.</p>
<h3 id="how-it-all-fits-together">How It All Fits Together</h3>
<p>Today’s tools (the linter and formatter) make <code>.html.erb</code> safer and easier to work with. But they’re also building blocks for tomorrow’s ReActionView.</p>
<p>The progression is natural:</p>
<ul>
<li><strong>Parser</strong> understands HTML+ERB structure</li>
<li><strong>Linter/Formatter</strong> enforces best practices and consistency</li>
<li><strong>HTML-aware rendering</strong> prevents invalid markup at compile time</li>
<li><strong>Reactive templates</strong> efficiently update only what changes</li>
<li><strong>Universal templates</strong> work on both server and client</li>
</ul>
<p>It’s a roadmap designed for gradual, non-disruptive adoption that can be progressively enhanced where needed that feels Rails-like.</p>
<h3 id="help-improve-the-herb-parser">Help Improve the Herb Parser</h3>
<p>While Herb already handles a wide range of real-world ERB templates, there’s always room to improve. You can help make Herb even better by running it on your own projects and reporting any issues it finds.</p>
<p>Simply run these commands in your project:</p>
<pre><code class="language-bash">cd your_project/
gem install herb
herb analyze . # analyze all *.html.erb in the current directory
herb analyze app/views # or just inside a specific directory
</code></pre>
<p>This will analyze your HTML+ERB files and highlight any edge cases or parser errors. If it breaks on your files, that’s great! Every edge case helps improve the parser and improve Herb for everyone.</p>
<p>If you have any failing files it’s either because you actually have a legit error in your view file or because the parser timed out while trying to parse your file. You will see that it generates a <code>.log</code> file which will have more details on why and how it failed.</p>
<p>If you are able to, I would appreciate if you could open an issue with the template content. If you can’t share the content of the view files directly, it would mean a lot if you could try to isolate/replicate the error.</p>
<h3 id="conclusion">Conclusion</h3>
<p>Back in April, Herb initially only released with the parser, but now it’s become a more complete toolkit for HTML+ERB files.</p>
<p>With the release of v0.4.0, we finally have the foundation for better developer experience in the Rails view layer: linting, formatting, diagnostics, and instant feedback. But more importantly, we have a clear path forward.</p>
<p>What HEEx is for Phoenix LiveView, Herb could be for Rails ReactionView. Rails revolutionized web development and continues to evolve, but its view layer has remained largely unchanged while frontend needs have evolved dramatically.</p>
<p>ReActionView isn’t meant to replace ActionView or ERB. It’s a chance to evolve it. Carefully, incrementally, and in a way that feels true to what makes Rails great. I’m planning on releasing a first prototype version of ReActionView at my <a href="https://2025.euruko.org">EuRuKo 2025</a> talk in September in Portugal.</p>
<p><img loading="lazy" src="/images/resized/euruko-8d00c0cd-1024x.png" data-pswp-src="/images/railsconf-2025-recap/euruko.png" data-pswp-width="1920" data-pswp-height="1080" class="rounded-xl border" alt="EuRuko 2025 Talk" /></p>
<p>The Prism parser had a big effect on Ruby internals and the Ruby tooling landscape. Now it’s time to bring that same care to the view layer.</p>
<p>With Herb, I believe we have a path to level up the view layer and think that Herb could have a similar effect for HTML Templating, tooling, and maybe even a new rendering engine.</p>
<p>If you missed the talk, you can find the <a href="https://speakerdeck.com/marcoroth/the-modern-view-layer-rails-deserves-a-vision-for-2025-and-beyond-at-railsconf-2025-philadelphia-pa">slides on Speakerdeck</a>. In the meantime, I’d love for you to try the tools, file issues, and join the conversation.</p>
<p>Thanks to everyone who came to the talk in-person, asked questions, shared their excitement, and encouraged the project over the past year. This is just the beginning.</p>
<p>Thank you!</p>
<p>- Marco</p>Marco RothHerb Language Server and Visual Studio Code Extension2025-06-20T18:00:00+00:002025-06-20T18:00:00+00:00repo://posts.collection/_posts/2025-06-19-herb-language-server.md<p>Back in April, <a href="https://marcoroth.dev/posts/introducing-herb">I introduced <em>Herb</em></a>, a new HTML-aware ERB parser. Herb was designed from the ground up to deeply understand <code>.html.erb</code> files, preserving both HTML and embedded Ruby structure without losing any details.</p>
<p>It laid the technical foundation for a modern, robust developer experience around HTML+ERB. But at the time, Herb was mostly “infrastructure”, useful for tool builders, but not something most developers could immediately plug into their daily workflow and make use of.</p>
<p>Today, that changes.</p>
<p>I’m excited to announce the release of the <strong>Herb Language Server</strong> and the <strong>Visual Studio Code Extension</strong>.</p>
<p><img loading="lazy" src="/images/resized/herb-language-server-bdc3c93d-1024x.png" data-pswp-src="/images/herb/herb-language-server.png" data-pswp-width="2560" data-pswp-height="1280" class="rounded-xl border" alt="Herb HTML+ERB Language Tools" /></p>
<hr />
<h3 id="bringing-herb-to-your-editor">Bringing Herb to Your Editor</h3>
<p>With the new Herb Language Server and Visual Studio Code Extension, Herb is now directly usable in your everyday development environment.</p>
<ul>
<li><strong>Diagnostics:</strong> Catch unclosed tags, mismatched elements, invalid nesting, and ERB syntax issues</li>
<li><strong>Instant Feedback:</strong> See errors and warnings as you type, not after you reload your app or run a linter</li>
<li><strong>Whitespace-Aware Formatting (soon):</strong> Because Herb preserves exact structure and can fully understand every detail about your view files, it will be able to quickly auto-format HTML+ERB files on save.</li>
</ul>
<p>Unlike many existing tools, Herb tries to treat HTML+ERB templates as they are: a mix of markup and Ruby code. This makes it possible to provide feedback that’s a bit more aware of how HTML+ERB views are usually written.</p>
<p>Right now, we only provide a pre-packaged Visual Studio Code extension. But since the language server follows the standard Language Server Protocol (LSP), it can be integrated into any editor that supports LSP, including Vim, Neovim, Zed, RubyMine, and many others.</p>
<p>If you’re interested in helping build or improve integrations for other editors, I’d be happy to collaborate.</p>
<hr />
<h3 id="why-this-matters">Why This Matters</h3>
<p>Ruby and Rails developers write a <em>lot</em> of HTML+ERB. Especially now with all the popularity around Hotwire, Turbo, Stimulus, HTMX and similar tools.</p>
<p>And yet, for years, ERB tooling has lagged behind nearly every other part of the modern development stack. No great formatter. Limited linting. Barely any language-server support. And definitely no structural understanding of how HTML and Ruby interleave.</p>
<p>Herb changes that. With this release, we’re one step closer to giving Ruby developers the kind of real-time, context-aware feedback that frontend developers have come to expect from their TypeScript, JSX, React or Vue tooling.</p>
<hr />
<h3 id="easy-to-install-easy-to-use">Easy to Install, Easy to Use</h3>
<p>The Visual Studio Code Extension is available now on the Visual Studio Code Marketplace.</p>
<ul>
<li>Search for <strong>“Herb LSP - HTML+ERB Language Tools”</strong> in VS Code extensions</li>
<li>Install and open any <code>.html.erb</code> file</li>
<li>The extension activates automatically</li>
</ul>
<p>The language server is fully open source, built directly on top of Herb’s C-powered parser with the JavaScript/Node.js and WebAssembly bindings.</p>
<hr />
<h3 id="a-foundation-for-more">A Foundation for More</h3>
<p>This is just the beginning. With Herb’s parser and language server in place, future additions are already on the roadmap:</p>
<ul>
<li>Auto-formatting with safe whitespace-preserving transformations</li>
<li>Better inline go-to-definition and symbol navigation for embedded Ruby</li>
<li>Cross-file component and partial analysis</li>
<li>Turbo and Stimulus LSP integration</li>
<li>Rails-aware template diagnostics</li>
<li>and a lot more!</li>
</ul>
<p>The long-term goal remains the same: make <code>.html.erb</code> files as productive, reliable, and enjoyable to work with as any modern frontend framework.</p>
<hr />
<h3 id="a-step-toward-broader-integration">A Step Toward Broader Integration</h3>
<p>This release is just the first step in making Herb more widely available. The goal is to get it into the hands of developers so it can be tested, battle-hardened, and refined against real-world HTML+ERB code.</p>
<p>Long-term, I hope Herb won’t stay as a standalone language server forever. My goal is to collaborate with other tool developers and see Herb’s parser integrated wherever it makes sense, inside formatters, linters, Rails developer tools, and ideally directly into existing tools like the Ruby LSP.</p>
<p>In the best case, one day this standalone Herb Language Server won’t even need to exist because its capabilities will have been folded into the standard Ruby tooling experience, no extra installs, no configuration, just better HTML+ERB support by default.</p>
<hr />
<h3 id="help-improve-the-herb-parser">Help Improve the Herb Parser</h3>
<p>In the meantime, while Herb already handles a wide range of real-world ERB templates, there’s always room to improve. You can help make Herb even better by running it on your own projects and reporting any issues it finds.</p>
<p>Simply run these commands in your project:</p>
<pre><code class="language-bash">cd your_project/
gem install herb
herb analyze . # analyze all *.html.erb in the current directory
herb analyze app/views # or just inside a specific directory
</code></pre>
<p>This will analyze your HTML+ERB files and highlight any edge cases or parser errors. If it breaks on your files, that’s great! Every edge case helps improve the parser and strengthen Herb for everyone.</p>
<p>If you have any failing files it’s either because you actually have a legit error in your view file or because the parser timed out while trying to parse your file. You will see that it generates a <code>.log</code> file which will have more details.</p>
<p>If you are able to, I would appreciate if you could open an issue with the template content. If you can’t share the content of the view files directly, it would mean a lot if you could try to isolate/replicate the error.</p>
<hr />
<h3 id="try-it-today">Try It Today</h3>
<p>If you write HTML+ERB views, I encourage you to try it out.</p>
<ul>
<li>Visual Studio Code Extension: <a href="https://marketplace.visualstudio.com/items?itemName=marcoroth.herb-lsp">Herb LSP - HTML+ERB Language Tools</a></li>
<li>Herb Language Server: <a href="https://github.com/marcoroth/herb/tree/main/javascript/packages/language-server">GitHub repo</a></li>
</ul>
<p>As always, feedback is welcome and appreciated. This is a community effort, and the more real-world projects Herb sees, the better the tools will become.</p>
<hr />
<h3 id="podcast-appearance-talking-about-herb">Podcast Appearance: Talking about Herb</h3>
<p>If you’re curious to hear a bit more of the background story behind Herb, I recently joined Jared Norman on his podcast <em>Dead Code</em> to talk about the project, developer tooling, and parsing in general.</p>
<p>You can listen to the episode here: <a href="https://shows.acast.com/dead-code/episodes/herbicide-with-marco-roth">Herbicide with Marco Roth</a>.</p>
<hr />
<p>Thanks to everyone who supported the initial Herb parser launch. With the Language Server and Visual Studio Code extension, we’re finally turning that low-level parser into something you can see and feel, every day, inside your editor.</p>
<p>Marco</p>Marco RothIntroducing Herb: A new HTML-Aware ERB Parser for smarter developer tooling2025-04-16T05:00:00+00:002025-04-16T05:00:00+00:00repo://posts.collection/_posts/2025-04-16-introducing-herb.md<hr />
<p><em>Announced on April 16 at RubyKaigi 2025 in Matsuyama, Japan.</em></p>
<p>Today at <a href="https://rubykaigi.org/2025">RubyKaigi 2025</a>, I’m excited to introduce a project I’ve been heavily focusing over the last few months:</p>
<p>🌿 <strong>Herb</strong> — a fast, modern, and HTML-aware ERB parser, designed from the ground up for developer tooling.</p>
<hr />
<h2 id="why-herb">Why Herb?</h2>
<p>As web development in Ruby evolves - especially with the rise of Hotwire and HTML-over-the-wire patterns - it’s become clear that traditional ERB tooling hasn’t kept up.</p>
<p>Rails has always been a joy to work with and always stood out in it’s developer experience and always had a strong view layer, centered around <strong>ERB templates</strong>.</p>
<p>With the introduction of <strong>Hotwire</strong> and HTML-over-the-wire becoming the default in Rails 7+, we’re writing <strong>more HTML+ERB than ever</strong></p>
<p>Herb is designed to bridge that gap. It brings real-time, editor-friendly parsing capabilities to ERB, enabling a wave of new tools like formatters, linters, and language server integrations (LSPs).</p>
<p>Herb is fast, fault-tolerant, and deeply aware of the HTML structure. It’s built for developers and tooling developers who want precise, reliable tooling without compromise.</p>
<p>Even as Ruby developer tools have improved — thanks to projects like <a href="https://github.com/Shopify/ruby-lsp">Shopify’s Ruby LSP</a> and <a href="https://github.com/ruby/prism">Prism</a> — view files have largely been left behind. HTML embedded in <code>.html.erb</code> files remains a black box:</p>
<ul>
<li>No (or limited) jump-to-definition for view helpers</li>
<li>No way of understanding the HTML semantics in view files.</li>
<li>No syntax-aware formatting or linting</li>
<li>No way to understand what ActionView’s <code>form_with</code>, <code>link_to</code>, or <code>render</code> will output</li>
<li>No editor support for HTML-aware ERB analysis or transformation</li>
</ul>
<p>In the <a href="https://railsdeveloper.com/survey/2024/#what-javascript-libraries-are-you-using-alongside-rails">2024 Rails Developer Survey</a> Stimulus climbed to the number one spot in the JavaScript frameworks sections.</p>
<p><img loading="lazy" src="/images/resized/rails-developer-survey-affb7425-1024x.png" data-pswp-src="/images/herb/rails-developer-survey.png" data-pswp-width="1992" data-pswp-height="1374" class="rounded-xl border" alt="Stimulus in the Rails Developer Survey 2024 with 31%" /></p>
<p>This is especially limiting for modern Rails apps, where <strong>Stimulus</strong>, <strong>Turbo</strong>, and <strong>Hotwire</strong> all rely heavily on writing rich HTML on the server.</p>
<hr />
<h2 id="a-parser-built-for-tooling">A Parser Built for Tooling</h2>
<p>That’s where <strong>Herb</strong> comes in.</p>
<p>Herb is a <strong>new parser</strong> written in <strong>C</strong>, purpose-built to understand interleaved <strong>HTML and ERB</strong>. It gives tools the ability to reason about HTML structures even when they’re dynamically generated with Ruby helpers and nested in ERB tags.</p>
<p>Herb is:</p>
<ul>
<li><strong>HTML-aware</strong>, with a purpose-built lexer and parser</li>
<li><strong>ERB-aware</strong>, with integration points to Ruby’s official Prism parser</li>
<li><strong>Error-tolerant</strong>, to support real-world code and in-editor feedback</li>
<li><strong>Whitespace- and structure-preserving</strong>, critical for formatters and linters</li>
<li><strong>Built for speed</strong>, enabling real-time updates in developer tools</li>
<li><strong>Designed to be embeddable</strong>, with bindings for Ruby, JavaScript, and more</li>
</ul>
<p>Herb produces a full <strong>AST</strong> representing the structure of HTML + ERB templates, making it ideal for:</p>
<ul>
<li>Building ERB-aware <strong>formatters</strong> and <strong>linters</strong></li>
<li>Writing intelligent <strong>LSP features</strong> (e.g. autocomplete inside ERB)</li>
<li>Performing <strong>static analysis</strong> on server-rendered markup</li>
<li>Refactoring or transforming views at scale</li>
<li>Highlighting issues with tag mismatches or invalid nesting</li>
</ul>
<hr />
<h2 id="why-not-nokogiri">Why Not Nokogiri?</h2>
<p>You might ask: why not just use <strong>Nokogiri</strong>?</p>
<p>Nokogiri is great for working with HTML — but it’s an HTML-compliant parser. That means it <em>fixes</em> your mistakes (autocloses tags, moves nodes around) and doesn’t preserve exactly what’s written in your <code>.html.erb</code> files. That’s how it’s intended to work and what you would expected from a HTML5 parser.</p>
<p>Don’t get me wrong, Nokogiri is an awesome tool and is perfect when trying to parse HTML as browsers would, but it sadly doesn’t work great for developer tooling.</p>
<p>With Herb, what you parse is exactly what you wrote. No assumptions. No transformations. Just the raw, structured syntax tree of your view template — including whitespace, comments, and malformed fragments.</p>
<p>This allows us to exactly see what you’ve written in your view files and allows us analyze the source as it it is. This also allows use to write tools like formatters and linters to annotate the view files with diagnostics to help you find syntax or semantic errors.</p>
<hr />
<h2 id="whats-next">What’s Next</h2>
<p>Today is just the beginning as with the release of the parser we now have the foundation to build more advanced tools.</p>
<p>For now, Herb focused on <strong>empowering developer tools</strong>: LSPs, linters, formatters, analyzers, refactoring tools — the kind of tools we’ve long had in other ecosystems, but that Ruby, and HTML+ERB specifically, has lacked in the view layer.</p>
<p>Now with this parser released, we are looking to improve existing tools in our ecosystem, including:</p>
<ul>
<li><a href="http://github.com/marcoroth/stimulus-lsp"><strong>Stimulus LSP</strong></a></li>
<li><a href="http://github.com/marcoroth/stimulus-lsp"><strong>Stimulus Lint</strong></a></li>
<li><a href="http://github.com/marcoroth/turbo-lsp"><strong>Turbo LSP</strong></a></li>
<li><a href="http://github.com/shopify/ruby-lsp"><strong>Ruby LSP</strong></a></li>
<li><a href="http://github.com/marcoroth/phlexing"><strong>Phlexing</strong></a></li>
<li><a href="http://github.com/marcoroth/herb"><strong>Herb Auto-Formatter</strong></a></li>
<li><a href="http://github.com/marcoroth/herb"><strong>Herb Linter</strong></a></li>
</ul>
<p>and possibly more!</p>
<hr />
<h2 id="getting-started">Getting Started</h2>
<h3 id="ruby">Ruby</h3>
<p>Herb is bundled and packaged up as a precompiled RubyGem and available to be installed from RubyGems.org.</p>
<p>Add <code>gem "herb"</code> to your <code>Gemfile</code> and call <code>Herb.parse_file()</code> to get started.</p>
<pre><code class="language-ruby">require "herb"
Herb.parse_file("path/to/view.html.erb")
</code></pre>
<p>Check out the <a href="https://herb-tools.dev/bindings/ruby">Ruby bindings documentation</a> to learn more.</p>
<h3 id="javascript">JavaScript</h3>
<p>We are also shipping two NPM packages for the use in Node.js and a WebAssembly build for the use in the browser. You can install the <code>@herb-tools/browser</code> or <code>@herb-tools/node</code> package from NPM respectively.</p>
<p>Check out the <a href="https://herb-tools.dev/bindings/javascript">JavaScript bindings documentation</a> to learn more.</p>
<h3 id="playground">Playground</h3>
<p>We also have an interactive browser-based parser playground to explore how the parser works.</p>
<p>The playground uses the <code>@herb-tools/browser</code> and WebAssembly build to run the parser fully in the browser.</p>
<p>Check out the <a href="https://herb-tools.dev/playground">interactive playground</a> to play with the parser output yourself.</p>
<hr />
<h3 id="summary">Summary</h3>
<p>For my RubyKaigi talk I prepared a summary slide. Here is the most important information from this talk on one slide:</p>
<p><img loading="lazy" src="/images/resized/summary-14202bb4-1024x.png" data-pswp-src="/images/herb/summary.png" data-pswp-width="3200" data-pswp-height="1800" class="rounded-xl border" alt="Summary Slide from my RubyKaigi 2025 talk" /></p>
<hr />
<h2 id="a-gateway-to-better-tooling">A Gateway to Better Tooling</h2>
<p>Herb isn’t just a parser - it’s a foundation for better HTML Templating Tooling. A better HTML+ERB Syntax Tree, better HTML+ERB editor support, and meant to level up the developer experience across the board.</p>
<p>Whether you’re building modern Rails apps or maintaining legacy templates, Herb will bring clarity, structure, and modern ergonomics to your HTML+ERB views.</p>
<p>I’m excited to share this with the Ruby community today and look forward to seeing how you use it.</p>
<p>→ <a href="https://herb-tools.dev">View the documentation</a><br />
→ <a href="https://herb-tools.dev/playground.html">Try the playground</a><br />
→ <a href="https://github.com/marcoroth/herb">GitHub repo</a></p>
<p>I’m planning to write more about Herb in the next few weeks to touch on the technical details, architecture, design-decisions, differences and more!</p>
<p>Let’s build the tooling the modern Rails view layer deserves. 🌿</p>
<p>— Marco</p>Marco RothIntroducing RubyEvents.org: RubyConferences.org and RubyVideo.dev Join Forces to Build the Ruby Events Platform2025-04-03T15:00:00+00:002025-04-03T15:00:00+00:00repo://posts.collection/_posts/2025-04-03-introducing-rubyevents-org.md<p>Over the years, <a href="https://rubyconferences.org"><strong>RubyConferences.org</strong></a> has been a trusted source for discovering upcoming Ruby conferences worldwide. Meanwhile, <strong>RubyVideo.dev</strong> has grown into an extensive archive of Ruby conference talks and events, aiming to preserve and share the knowledge shared on stage by the community.</p>
<p>Today, we’re excited to announce the next chapter:
<strong>RubyConferences.org and RubyVideo.dev are merging into a new, unified platform - <a href="https://rubyevents.org">RubyEvents.org</a>.</strong></p>
<p>RubyEvents.org is built to be the <strong>central platform for all things Ruby events</strong> - not just conferences, but also meetups, workshops, hackathons, CFPs, and more. Whether you’re a speaker, attendee, organizer, or sponsor, RubyEvents.org is designed to serve your needs.</p>
<hr />
<h2 id="why-merge">Why Merge?</h2>
<p>RubyConferences.org started with listing Ruby conferences and Call For Papers, and has been the go-to resource over the past decade. In 2024 we also added the support to keep track of Ruby meetups around the world.</p>
<p><a href="https://x.com/adrienpoly/status/1669944383049801730">Adrien Poly started RubyVideo.dev in 2023</a> with the ambition to aggregate all Ruby-related videos in one place with searching capabilities to facilitate discovery, all while showcasing some of the new Rails technology.</p>
<p>Over the last two years these sites became closer and closer and created some duplicate effort to keep both sides up-to-date. By bringing them together, we can:</p>
<ul>
<li>Offer a <strong>single place to discover and explore Ruby events</strong></li>
<li><strong>Archive</strong> slides, photos, and recordings alongside each event’s details</li>
<li>Create <strong>public speaker profiles</strong> with talk histories</li>
<li>Help <strong>meetup organizers</strong> find speakers and promote their events</li>
<li>Provide <strong>conference organizers</strong> with tools to run CFPs, attract sponsors, and build visibility</li>
<li>Give <strong>sponsors</strong> a way to showcase their community involvement across multiple events</li>
</ul>
<hr />
<h2 id="a-new-home-for-the-ruby-communitys-event-history">A New Home for the Ruby Community’s Event History</h2>
<p>Many historical Ruby conference websites have disappeared over time and aren’t accessible anymore, making it challenging to access important information about these events. Details such as speaker lineups, dates, venues, and other valuable context are often lost to the digital void, erasing pieces of our community’s history.</p>
<p>With RubyEvents.org, past events aren’t lost to time - quite the opposite. The platform preserves and expands upon the comprehensive event archive from RubyConferences.org while integrating the rich talk data from RubyVideo.dev. This creates not just an accurate historical record, but a vibrant archive that maintains the unique identity of each event through their original assets, including event logos, banners, branding, and more!</p>
<p>Each event gets a dedicated page, and over time we’ll be adding recordings, speaker information, and more. If you’ve spoken at or organized a Ruby event in the past, your contributions now have a way of being archived and preserved in the RubyEvents.org archive. If they aren’t on RubyEvents.org yet, feel free to <a href="https://github.com/adrienpoly/rubyvideo/issues/new">open an issue</a> or even better, contribute it to <a href="https://github.com/adrienpoly/rubyvideo">RubyEvents.org</a>.</p>
<hr />
<h2 id="whats-next">What’s Next?</h2>
<p>This is just the beginning.</p>
<p>We’re actively collaborating with conference and meetup organizers to evolve RubyEvents.org into a comprehensive platform that supports the entire lifecycle of Ruby events. From managing Call for Papers and publishing announcements, to displaying event schedules, archiving talk recordings, and sharing post-event updates - our goal is to create a seamless experience for everyone involved in the Ruby community’s events ecosystem.</p>
<p>If you’re running a meetup or event, we’d love to help you list it. If you’ve given a talk, we want to index it. If you’re a company that’s supporting Ruby events, we want to help you get the recognition you deserve for supporting our awesome Ruby community.</p>
<p>We’re also excited to share that we have been building <strong>a Hotwire Native RubyEvents.org iOS app</strong> that’s completely open-source. It serves not just as a companion app, but as a <strong>reference application</strong> for building modern, real-world Rails apps and leveraging <a href="https://native.hotwired.dev">Hotwire Native</a> to create powerful mobile experiences.</p>
<p>👉 <a href="/posts/introducing-rubyevents-ios">Read more about the iOS app</a></p>
<p>Our goal is to make this app a learning resource for anyone interested in seeing how the latest tools in the Rails ecosystem can come together in a production-quality application. We also want to create opportunities for early-career developers to contribute to a real-world app that people actually use. If you’re interested in contributing, check out the <a href="https://github.com/adrienpoly/rubyvideo">GitHub repo</a> and/or visit the <a href="https://www.rubyevents.org/contributions">Contributions Page</a> on RubyEvents.org.</p>
<p>Please reach out if you don’t know where to start or need any guidance, I would be more than happy to help you get your first contribution to RubyEvents.org!</p>
<hr />
<p><strong>Thank you</strong> to everyone who’s supported RubyConferences.org and RubyVideo.dev over the years. We’re excited to take this next step together, and we hope RubyEvents.org becomes a valuable home for the whole Ruby community.</p>
<p>Special thanks to <a href="http://jonallured.com/">Jon Allured</a> and <a href="https://www.camerondaigle.com/">Cameron Daigle</a> for creating, maintaining, and stewarding RubyConferences.org over the years and to <a href="https://github.com/adrienpoly">Adrien Poly</a> for starting RubyVideo.dev in the first place. This project wouldn’t exist without their foundational work.</p>
<p>If you have feedback, ideas, or want to get involved, feel free to reach out.</p>
<p>You can reach us on <a href="https://bsky.app/profile/rubyevents.org">Bluesky</a>, <a href="https://x.com/rubyevents_org">Twitter/X</a>, <a href="https://elk.zone/ruby.social/@RubyEvents">Mastodon</a>, or <a href="http://linkedin.com/company/rubyevents">LinkedIn</a>.</p>
<p>Thank you!</p>Marco RothIntroducing the RubyEvents.org iOS App2025-04-03T15:00:00+00:002025-04-03T15:00:00+00:00repo://posts.collection/_posts/2025-04-03-introducing-rubyevents-ios.md<p>As announced in <a href="/posts/introducing-rubyevents-org">“Introducing RubyEvents.org: RubyConferences.org and RubyVideo.dev Join Forces”</a> — we are in the process of merging two long-standing community efforts into a single home for all things Ruby events.</p>
<p>To mark this new beginning, we’re excited to share another major milestone:</p>
<p><strong>Say hello to the RubyEvents.org iOS App</strong>, built with <a href="https://native.hotwired.dev/">Hotwire Native</a>.</p>
<hr />
<h2 id="ruby-is-back--and-so-are-the-meetups">Ruby is Back — And So Are the Meetups</h2>
<p>We’re seeing the <strong>reboot and rise of Ruby meetups around the world</strong>, and for the first time since 2015, we’re breaking records again.</p>
<p><img loading="lazy" src="/images/resized/yearly-talks-ef9168a8-1024x.png" data-pswp-src="/images/rubyevents/yearly-talks.png" data-pswp-width="3840" data-pswp-height="1940" class="rounded-xl border" alt="2024 is the year with the most Ruby talks in a single year so far." /></p>
<p>With all this momentum, we wanted a better way to keep up with what’s happening — whether it’s a meetup, conference, workshop, or hackathon. The RubyEvents.org app brings it right to your phone.</p>
<hr />
<h2 id="a-native-experience-powered-by-rails">A Native Experience, Powered by Rails</h2>
<p>This app is also meant as a <strong>public resource</strong> and a <strong>reference project</strong> for how to build modern mobile apps using Rails.</p>
<p>This architecture gives you the best of both worlds: server-rendered HTML for fast iteration and low complexity, plus native UI when and where you want it. Having more open-source Hotwire Native apps is going to be value for the community.</p>
<hr />
<h2 id="built-for-the-community">Built for the Community</h2>
<p>This app is meant to <strong>celebrate the launch of RubyEvents.org</strong> and kick off a new era of Ruby community infrastructure. But it’s only the beginning.</p>
<p>RubyEvents.org is an open platform, and so is the app. We welcome contributions, ideas, and improvements — whether you’re a designer, organizer, mobile dev, or just someone excited about Ruby’s future.</p>
<p>We’re also <strong>working on an Android version</strong> of the app and would love help bringing it to life. If you’re familiar with Android or just interested in learning, this could be a great way to get involved too.</p>
<hr />
<h2 id="get-involved">Get Involved</h2>
<p>The RubyEvents.org app isn’t just for finding events — it’s about supporting the people behind them.</p>
<p>We hope it becomes a tool to connect speakers with meetups, organizers with sponsors, and attendees with the vibrant Ruby community near them (or around the world).</p>
<p>If you want to contribute, suggest a feature, or just follow along, check out the <a href="https://github.com/rubyevents"><strong>GitHub organization</strong></a> to place feature request, share feedback or contribute.</p>
<hr />
<h2 id="download-the-app-on-the-app-store">Download the App on the App Store</h2>
<p>The RubyEvents.org iOS app is now available on the App Store.<br />
Start exploring Ruby events near you — and around the world.</p>
<p><img loading="lazy" src="/images/resized/app-store-5ef0de6e-1024x.png" data-pswp-src="/images/rubyevents/app-store.png" data-pswp-width="2206" data-pswp-height="1241" class="rounded-xl border" alt="RubyEvents app for iOS, now available." /></p>
<p>➡️ <a href="https://apps.apple.com/app/rubyevents/id6743987375">Download the app on the App Store</a></p>
<hr />
<p>This is just the start. Please get in touch if you have any questions or want to help out.</p>
<p>You can reach us on <a href="https://bsky.app/profile/rubyevents.org">Bluesky</a>, <a href="https://x.com/rubyevents_org">Twitter/X</a>, <a href="https://elk.zone/ruby.social/@RubyEvents">Mastodon</a>, or <a href="http://linkedin.com/company/rubyevents">LinkedIn</a>.</p>
<p>Let’s build the future of Ruby events — together.</p>
<p>– The RubyEvents.org Team</p>Marco Roth2024 Year-in-Review2025-01-23T15:00:00+00:002025-01-23T15:00:00+00:00repo://posts.collection/_posts/2025-01-18-2024-year-in-review.md<p>As 2024 recently wrapped up, I wanted to take a moment to reflect on the year.</p>
<p>Personally, I’m not a big believer in strict New Year’s resolutions. However, I see value in setting long-term goals and taking time to reflect on what I’ve accomplished, learned, and where there’s room for improvement.</p>
<p>2024 has been a year to remember — packed with travel, projects, and a lot of contributions to the community.</p>
<h2 id="highlights-of-the-year">Highlights of the Year</h2>
<h3 id="conferences">Conferences</h3>
<p>In 2024, I attended <strong>14 conferences</strong> across several countries and continents.</p>
<div class="grid grid-cols-3 md:grid-cols-5 gap-2 no-captions-on-mobile">
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/sincityruby1-fe58cac-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/sincityruby1.jpg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md object-cover w-full h-full" alt="Sin City Ruby" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/sincityruby2-21a433fb-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/sincityruby2.jpg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md object-cover w-full h-full" alt="Sin City Ruby" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/sincityruby3-a9a65813-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/sincityruby3.jpg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md object-cover w-full h-full" alt="Sin City Ruby" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/railsconf-f120e122-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/railsconf.jpg" data-pswp-width="4805" data-pswp-height="3203" class="rounded-md object-cover w-full h-full" alt="RailsConf" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/balticruby-89493887-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/balticruby.jpg" data-pswp-width="2048" data-pswp-height="1542" class="rounded-md object-cover w-full h-full" alt="Baltic Ruby" />
</div>
</div>
<p>What stood out to me was the openness and energy of the Ruby community. Every event felt like a mini-reunion with old friends which lead to a lot of new memories. But it also lead to awesome conversations with new friends, that I’m happy to come back to in 2025.</p>
<div class="grid grid-cols-3 md:grid-cols-5 gap-2 no-captions-on-mobile">
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/madison-ruby-94cac8a-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/madison-ruby.jpg" data-pswp-width="4000" data-pswp-height="3008" class="rounded-md object-cover w-full h-full" alt="Madison+ Ruby" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/euruko-82f51337-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/euruko.jpg" data-pswp-width="2048" data-pswp-height="1536" class="rounded-md object-cover w-full h-full" alt="EuRuKo" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/friendlyrb1-54fa07ad-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/friendlyrb1.jpg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md object-cover w-full h-full" alt="Friendly.rb" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/friendlyrb2-63d7d3d1-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/friendlyrb2.jpg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md object-cover w-full h-full" alt="Friendly.rb" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/friendlyrb3-bdbc08c4-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/friendlyrb3.jpg" data-pswp-width="2048" data-pswp-height="1536" class="rounded-md object-cover w-full h-full" alt="Friendly.rb" />
</div>
</div>
<p>Each conference offered something unique. From technical deep dives in talks to interesting discussions in the hallway track. I learned how much energy and inspiration I gain from the in-person aspect of these events. For me it’s a feeling that virtual events or working remotely can’t quite replicate — which in turn is why I’m attending so many conferences.</p>
<p>Looking back, I couldn’t have asked for a better year of conferences. 2024 set an incredibly high bar, and while it’ll be tough to match in 2025, I’m excited to see what’s coming in 2025.</p>
<div class="grid grid-cols-3 md:grid-cols-5 gap-2 no-captions-on-mobile">
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/rails-world1-9cf4d1a0-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/rails-world1.jpg" data-pswp-width="2048" data-pswp-height="1536" class="rounded-md object-cover w-full h-full" alt="Rails World" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/rails-world2-96dd9988-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/rails-world2.jpg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md object-cover w-full h-full" alt="Rails World" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/rails-world3-483af832-256x.jpg" data-pswp-src="/images/2024-year-in-review/conferences/rails-world3.jpg" data-pswp-width="2048" data-pswp-height="1536" class="rounded-md object-cover w-full h-full" alt="Rails World" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/rubyconf1-6563ec95-256x.png" data-pswp-src="/images/2024-year-in-review/conferences/rubyconf1.png" data-pswp-width="3000" data-pswp-height="2250" class="rounded-md object-cover w-full h-full" alt="RubyConf" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/rubyconf2-ec9da1de-256x.png" data-pswp-src="/images/2024-year-in-review/conferences/rubyconf2.png" data-pswp-width="1536" data-pswp-height="1324" class="rounded-md object-cover w-full h-full" alt="RubyConf" />
</div>
</div>
<hr />
<h3 id="talks">Talks</h3>
<p>I’ve written <strong>two new technical talks</strong> in 2024, both focused on Hotwire-related topics. These talks reflected my passion for shaping the developer experience within the Rails and Hotwire ecosystems:</p>
<ul>
<li>
<p><strong>Revisiting the Hotwire Landscape after Turbo 8</strong><br />
This talk reflected on how far Hotwire has come, how StimulusReflex and CableReady might have shaped the state of Hotwire today, where I think the Hotwire-approach is headed, and what I would love to see for the frameworks — both in terms of governance and features, but also in terms of vision.</p>
<p>I think that Hotwire is a gift for the Ruby, Rails, and even the wider web development communities. In a way I still think that Hotwire is what makes Ruby on Rails relevant as a Full-Stack framework today. But I also think that there is so much potential to level up the frameworks even more. I will give my best to help shape the future of Hotwire in 2025.</p>
</li>
<li>
<p><strong>Leveling Up Developer Tooling For The Modern Rails & Hotwire Era</strong> <br />
This was a more technical talk where I explained the story behind building the Stimulus Language Server. I wanted to share the story and timeline behind it, wanted to explain how LSPs work in general, how you can build a simple language server yourself. I also touched on how I think we can improve developer tooling to enhance the overall experience for Rails and Hotwire developers.</p>
</li>
</ul>
<p>Delivering these talks wasn’t just about sharing technical knowledge. A large part was about pushing myself outside my comfort zone to get more comfortable giving talks on stage in front of people after my <a href="/talks/rails-world-2023">talk debut in 2023</a>.</p>
<div class="grid grid-cols-2 gap-2">
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/railsconf-96419488-512x.jpg" data-pswp-src="/images/2024-year-in-review/talks/railsconf.jpg" data-pswp-width="3325" data-pswp-height="2494" class="rounded-md object-cover w-full h-full" alt="RailsConf (📸 Kevin Murphy)" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/brightonruby-ce77d2d9-512x.jpg" data-pswp-src="/images/2024-year-in-review/talks/brightonruby.jpg" data-pswp-width="1920" data-pswp-height="1280" class="rounded-md object-cover w-full h-full" alt="Brighton Ruby (📸 Brighton Ruby)" />
</div>
</div>
<p>Getting to give <a href="https://marcoroth.dev/talks">13 talks</a> (of which 8 were at conferences) in a single year is a huge privilege I’m super thankful for!</p>
<p>I am grateful for all the incredible opportunities to speak at these amazing events around the world. I’m also super thankful for all the event organizers who trusted me with their stages.</p>
<div class="grid grid-cols-2 gap-2">
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/reddotrubyconf-ffc86a8a-512x.jpg" data-pswp-src="/images/2024-year-in-review/talks/reddotrubyconf.jpg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md object-cover w-full h-full" alt="Red Dot Ruby Conf (📸 Onur Özer)" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/madison-ruby-a664b8d7-512x.jpg" data-pswp-src="/images/2024-year-in-review/talks/madison-ruby.jpg" data-pswp-width="6192" data-pswp-height="4128" class="rounded-md object-cover w-full h-full" alt="Madison+ Ruby (📸 Frannie Cina)" />
</div>
</div>
<p>A huge thanks to everyone for sharing your thoughts, feedback, and impressions, in-person and online. I’ve grown significantly as a speaker and learned to embrace sharing ideas on a stage. It’s something I’ve come to enjoy and look forward to continuing improving and leveling up in 2025.</p>
<div class="grid grid-cols-2 gap-2">
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/euruko-90e637ab-512x.jpg" data-pswp-src="/images/2024-year-in-review/talks/euruko.jpg" data-pswp-width="6000" data-pswp-height="4000" class="rounded-md object-cover w-full h-full" alt="EuRuKo (📸 EuRuKo)" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/rocky-mountain-ruby-dea9507-512x.jpg" data-pswp-src="/images/2024-year-in-review/talks/rocky-mountain-ruby.jpg" data-pswp-width="4080" data-pswp-height="3072" class="rounded-md object-cover w-full h-full" alt="Rocky Mountain Ruby (📸 Rocky Mountain Ruby)" />
</div>
</div>
<hr />
<h3 id="travel">Travel</h3>
<p>Traveling was a major highlight of 2024 for me. A lot of the travel was motivated by attending conferences and meetups.</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 no-captions-on-mobile">
<div>
<img loading="lazy" src="/images/resized/IMG_6813-cfdd111e-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_6813.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Los Angeles, CA" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_7152-38f4afd9-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_7152.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Mojave National Preserve, CA" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_7798-db4e4903-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_7798.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Death Valley National Park - CA" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_7946-6c8aa483-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_7946.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Death Valley National Park, CA" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_8548-25b0a2c1-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_8548.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Emerald Bay State Park, CA" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_8696-2abe67ab-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_8696.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Incline Village, NV" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_8310-3059d99e-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_8310.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="South Lake Tahoe, CA" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_0907-ff81e62a-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_0907.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Zürich, Switzerland" />
</div>
</div>
<p>Beyond the events themselves, I enjoy combining my travel plans with these events and stay a few days longer because it allows me to explore new places, work remotely, meet like-minded individuals, attend music events, and make new friends along the way.</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 no-captions-on-mobile">
<div>
<img loading="lazy" src="/images/resized/IMG_2392-fe3a9820-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_2392.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Singapore" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_6993-2e739247-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_6993.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Bușteni, Romania" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_8649-ef24479a-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_8649.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Niagara Falls, Canada" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_0039-ab89ed5c-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_0039.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Garden of the Gods Park - Colorado Springs, CO" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_9835-b38b580-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_9835.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Balanced Rock - Colorado Springs, CO" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_0829-f4acac57-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_0829.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Lily Lake - Estes Park, CO" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_1340-95400270-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_1340.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Rocky Mountain National Park, CO" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_2226-e057bd50-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_2226.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Grand Lake, CO" />
</div>
</div>
<p>My 2024 travling in numbers:</p>
<ul>
<li><strong>31 flights</strong></li>
<li><strong>15 countries</strong> visited</li>
<li><strong>7 new countries</strong> explored</li>
<li><strong>117 days</strong> traveling away from home</li>
</ul>
<p><img loading="lazy" src="/images/resized/flighty-e83fe79-1024x.png" data-pswp-src="/images/2024-year-in-review/travel/flighty.png" data-pswp-width="1449" data-pswp-height="1859" class="w-full md:w-[75%] mx-auto" alt="2024 Flighty Passport" /></p>
<p>It’s going to be hard to match (or even beat) this in 2025, but that’s not the goal here. I’m already looking forward to travel, explore more countries, and attend conferences while doing so. I hope to you see you at a conference in 2025!</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 no-captions-on-mobile">
<div>
<img loading="lazy" src="/images/resized/IMG_6167-698d3b70-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_6167.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Warsaw, Poland" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_7477-480a8a44-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_7477.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Las Vegas, NV" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_0286-a56d6d8d-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_0286.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Williamsburg, NY" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_0660-f024b4ef-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_0660.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Detroit, MI" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_1287-ea2bb919-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_1287.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Malmö, Sweden" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_1570-32ad5403-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_1570.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Copenhagen, Denmark" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_1849-458f63fe-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_1849.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Brighton, UK" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_4748-7292dc6-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_4748.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Singapore" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_5241-c342f526-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_5241.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Chiacgo, IL" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_5564-ddaa18a2-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_5564.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Madison, WI" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_7834-83d76822-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_7834.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="Toronto, Canada" />
</div>
<div>
<img loading="lazy" src="/images/resized/IMG_3126-1f35f069-256x.jpeg" data-pswp-src="/images/2024-year-in-review/travel/IMG_3126.jpeg" data-pswp-width="4032" data-pswp-height="3024" class="rounded-md" alt="San Francisco, CA" />
</div>
</div>
<p>Travel has become my favorite way to combine professional and personal growth, and I’m already looking forward to more exploration in 2025.</p>
<hr />
<h2 id="open-source">Open Source</h2>
<p>Open Source has always been central to my work in the last few years, and 2024 was no exception.</p>
<p><img loading="lazy" src="/images/resized/contributions-317df328-1024x.png" data-pswp-src="/images/2024-year-in-review/open-source/contributions.png" data-pswp-width="1846" data-pswp-height="314" class="rounded-md" alt="GitHub Contributions in 2024" /></p>
<h3 id="open-source-time-management">Open Source Time Management</h3>
<p>I never felt the need to track my time working on Open Source projects. For me, it’s something I enjoy, and tracking it would make it feel more like a chore than a hobby — exactly what I’m trying to avoid.</p>
<p>That changed when I discovered <a href="https://timingapp.com">Timing App</a>, which automatically tracks how you spend time on your computer by analyzing the currently active window and open files and storing the data in a SQLite database.</p>
<p>In mid-2023, I decided to give it a try and started tracking all my work. The experience has been surprisingly transparent and reliable.</p>
<p>You can create projects and let the app automatically assign it to a project based on custom rules you define. It’s an awesome tool and I learned to love it.</p>
<p>My main motivation was curiosity: I wanted to see where my time was going and how much I was spending on different activities. Especially Open Source, since I wasn’t tracking it previously.</p>
<p>Without this tool, I wouldn’t have a good idea of how much time I dedicate to Open Source.</p>
<p>2024 was my first full year of tracking 100% of my time, and it has revealed some interesting patterns — including the fact that I probably spend <em>way</em> too much time on Open Source 🙈.</p>
<p>Here a few stats:</p>
<ul>
<li><strong>2,667 contributions</strong></li>
<li>opened <strong>467 pull requests</strong></li>
<li>opened <strong>153 issues</strong></li>
<li>contributed to repos in <strong>30 different organizations</strong></li>
<li>authored <strong>1,368 commits</strong> on GitHub</li>
<li>spent over <strong>1,000 hours</strong> on Open Source</li>
</ul>
<p><img loading="lazy" src="/images/resized/top-projects-74f09caf-1024x.png" data-pswp-src="/images/2024-year-in-review/open-source/top-projects.png" data-pswp-width="1567" data-pswp-height="1158" class="border rounded-xl" alt="Time Spent on Open Source Projects in 2024" /></p>
<p>It’s interesting to see how my Open Source work naturally comes in waves. A lot of this comes from doing non-Open Source work, but a lot of these gaps also line up with my travels.</p>
<p>It really shows how motivated and excited I get to work on Open Source after returning home from events, which is part of what makes it a perfect balance for me.</p>
<p><img loading="lazy" src="/images/resized/breakdown-by-day-a1aae6c9-1024x.png" data-pswp-src="/images/2024-year-in-review/open-source/breakdown-by-day.png" data-pswp-width="1404" data-pswp-height="694" class="border rounded-xl" alt="Open Source Time spent by day in 2024" /></p>
<p>My goal was to spend 50% of my work time in 2024 on Open Source, which checks out by looking at these stats!</p>
<p>I certainly had a lot of fun working on Open Source in 2025 and I’m looking forward to keep up my contributions in 2025!</p>
<h3 id="hotwire-weekly">Hotwire Weekly</h3>
<p>Starting with <strong>1,097 subscribers</strong>, the newsletter grew to <strong>1,888 subscribers</strong> by the end of 2024. It’s been a successful year for <a href="https://hotwireweekly.com">Hotwire Weekly</a>. I was able to stick to the weekly cadence and published a total of 52 newsletter editions in 2024.</p>
<p><img loading="lazy" src="/images/resized/hotwire-weekly-31ce16d8-512x.png" data-pswp-src="/images/2024-year-in-review/open-source/hotwire-weekly.png" data-pswp-width="1238" data-pswp-height="788" class="border rounded-xl w-full md:w-3/4 mx-auto" alt="Hotwire Weekly Logo" /></p>
<p>Starting (and running) a newsletter without any prior experience has been one of the most challenging yet rewarding experiences I’ve had.</p>
<p>Curating content, staying consistent, and engaging with subscribers have pushed me to grow and helped me connect with a broader audience in general.</p>
<p>I’m glad I took the leap, even when it felt uncomfortable at times, but that’s a sign of learning and progress — let’s embrace it.</p>
<p>A huge thank you to everyone who’s supported this journey! In 2025, I plan to continue refining and expanding Hotwire Weekly.</p>
<h3 id="rubyvideodev">RubyVideo.dev</h3>
<p><a href="https://rubyvideo.dev">RubyVideo.dev</a> was a major focus for me in the second half of 2024. I’m proud of the progress we made.</p>
<p><img loading="lazy" src="/images/resized/card-2fc5081d-512x.png" data-pswp-src="/images/2024-year-in-review/rubyvideo/card.png" data-pswp-width="1200" data-pswp-height="700" class="border rounded-xl w-full md:w-3/4 mx-auto" alt="RubyVideo.dev Logo" /></p>
<p>It’s been super motivating to work on the website, which is why I have spent a lot of time fine-tuning design/details, creating event assets, building new features and lastly, adding events and talks to the website. Here are some interesting stats:</p>
<ul>
<li>Added <strong>208 conferences</strong></li>
<li>Added <strong>16 meetup organizations</strong></li>
<li>Added <strong>5,014 talks</strong></li>
<li>Added <strong>2,270 speaker profiles</strong></li>
<li>Designed <strong>13 new pages</strong> and feature mockups</li>
<li>Designed <strong>862 assets</strong> for <strong>172 events</strong> (hero sections, profile banners, avatars, cards, and social previews)</li>
</ul>
<div class="relative group border border-transparent">
<div class="max-h-[500px] overflow-y-hidden">
<img loading="lazy" src="/images/resized/assets-3546aa35-1024x.png" data-pswp-src="/images/2024-year-in-review/rubyvideo/assets.png" data-pswp-width="2922" data-pswp-height="11548" class="object-cover object-top w-full h-full rounded-xl border" alt="RubyVideo Event Assets" />
<div class="absolute bottom-0 left-0 w-full h-36 bg-gradient-to-t from-white to-transparent flex justify-center items-end pointer-events-none">
<button class="bg-white border rounded-md py-1 px-2 text-sm">Show all assets</button>
</div>
</div>
</div>
<p>Moreover, we added a bunch of new features that makes RubyVideo.dev as awesome as it is today. Here are some of the exciting features I worked on:</p>
<ul>
<li>Added an Events Page</li>
<li>Added an Event Detail Page</li>
<li>Introduced Talk Topics</li>
<li>Introduced Talk Languages</li>
<li>Introduced Talk Slides</li>
<li>Introduced Talk Summaries</li>
<li>Richer Speaker Profiles</li>
<li>New Design</li>
<li>Redesigned Home Page (including the new hero sections)</li>
<li>More Social Media Providers</li>
<li>New video providers (MP4 and Vimeo)</li>
<li>Richer Conference Metadata</li>
<li>Conference Artwork (hero sections, profile banners, avatars, cards, and social previews)</li>
<li>Introduced Conference Schedule Page</li>
<li>Introduced Conference Speaker Page</li>
<li>Introduced Contributions Page</li>
<li>Introduced Lightning Talks as first-class citizens</li>
<li>Added Lightning Talk thumbnails</li>
<li>Added Lightning Talk cues</li>
<li>Added Scheduled Talks</li>
</ul>
<p>But, the website came a long way. Here are some before/after comparisons. Looking at this now makes me realize how far the website came!</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/home-before-2f9d7651-256x.png" data-pswp-src="/images/2024-year-in-review/rubyvideo/home-before.png" data-pswp-width="3022" data-pswp-height="1918" class="rounded-md border w-full h-full object-cover" alt="Homepage Before" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/home-after-eb41e9d9-256x.png" data-pswp-src="/images/2024-year-in-review/rubyvideo/home-after.png" data-pswp-width="2998" data-pswp-height="1928" class="rounded-md border w-full h-full object-cover" alt="Homepage After" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/speaker-show-before-47731b67-256x.png" data-pswp-src="/images/2024-year-in-review/rubyvideo/speaker-show-before.png" data-pswp-width="2940" data-pswp-height="1958" class="rounded-md border w-full h-full object-cover" alt="Speaker Page Before" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/speaker-show-after-e82510ca-256x.png" data-pswp-src="/images/2024-year-in-review/rubyvideo/speaker-show-after.png" data-pswp-width="2940" data-pswp-height="1958" class="rounded-md border w-full h-full object-cover" alt="Speaker Page After" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/talks-show-before-278ffd1b-256x.png" data-pswp-src="/images/2024-year-in-review/rubyvideo/talks-show-before.png" data-pswp-width="2938" data-pswp-height="1956" class="rounded-md border w-full h-full object-cover" alt="Talk Page Before" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/talks-show-after-48ebabc9-256x.png" data-pswp-src="/images/2024-year-in-review/rubyvideo/talks-show-after.png" data-pswp-width="2940" data-pswp-height="1958" class="rounded-md border w-full h-full object-cover" alt="Talk Page After" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/event-show-15ccb4cf-256x.png" data-pswp-src="/images/2024-year-in-review/rubyvideo/event-show.png" data-pswp-width="1694" data-pswp-height="1370" class="rounded-md border w-full h-full object-cover" alt="Event Show" />
</div>
<div class="aspect-[4/3]">
<img loading="lazy" src="/images/resized/event-schedule-e9b41fd4-256x.png" data-pswp-src="/images/2024-year-in-review/rubyvideo/event-schedule.png" data-pswp-width="1694" data-pswp-height="1370" class="rounded-md border w-full h-full object-cover" alt="Event Schedule" />
</div>
</div>
<p>RubyVideo.dev is becoming the go-to resource for Ruby events content, and I’m excited to keep growing it in 2025. If you want to help out, there’s a <a href="https://www.rubyvideo.dev/contributions">Contributions Page</a> to see where and how you can help. I’m more than happy to help you get started and involved, reach out!</p>
<h3 id="rubyconferencesorg">RubyConferences.org</h3>
<p>As mentioned above, 2024 has been an incredible year for conferences. Not just for myself, but for the Ruby Community as a whole!</p>
<p>We tracked and added <strong>38 Ruby Conferences</strong> on <a href="https://rubyconferences.org">RubyConferences.org</a> in 2024. I also introduced a new meetups section to RubyConferences.org in 2024, in which we tracked over <strong>500 Ruby meetups</strong>.</p>
<p>2024 quite literally has been the best year for Ruby events ever. It’s the first time we beat the previous all-time record from 2015 in terms of talks given at Ruby events in 2024!</p>
<p><img loading="lazy" src="/images/resized/yearly-talks-568712de-1024x.png" data-pswp-src="/images/2024-year-in-review/rubyvideo/yearly-talks.png" data-pswp-width="1558" data-pswp-height="786" class="rounded-xl border" alt="Yearly Talks at Ruby events" /></p>
<p>We’ve been working hard on something in this space that I’m hoping to be able to reveal more about soon. Stay tuned! Exiting times ahead.</p>
<p>All this makes me really proud to be a Rubyist! ❤️</p>
<h3 id="other-open-source-highlights">Other Open Source Highlights</h3>
<ul>
<li>Released <a href="https://github.com/stimulusreflex/stimulus_reflex/releases/tag/v3.5.0">StimulusReflex 3.5</a> (after a 3-year journey, involving 30+ contributors and 168 pull requests)</li>
<li>Released <a href="https://github.com/marcoroth/stimulus-lsp/releases/tag/v1.0.0">Stimulus LSP 1.0</a>, with a lot of new features and improvements.</li>
</ul>
<p><img loading="lazy" src="/images/resized/stimulus-lsp-68e9268-1024x.png" data-pswp-src="/images/2024-year-in-review/open-source/stimulus-lsp.png" data-pswp-width="3200" data-pswp-height="1800" class="rounded-xl pl-7" alt="Stimulus LSP 1.0" /></p>
<ul>
<li>Extracted and released <a href="https://github.com/marcoroth/stimulus-parser">Stimulus Parser</a> (written from the ground up with a focus on accuracy to reduced false-positives).</li>
<li>Started extracting Stimulus Lint, a CLI tool to run the Stimulus LSP diagnostics independently.</li>
<li>Released an initial version of <a href="https://github.com/marcoroth/turbo-lsp">Turbo LSP</a>.</li>
<li>Started working on my HTML-aware ERB Parser, built with developer tooling in mind. It should help improve the HTML+ERB tooling in LSPs and Ruby applications.</li>
<li>Released the <a href="https://github.com/marcoroth/qr_code_scanner"><code>qr_code_scanner</code></a> and <a href="https://github.com/marcoroth/swiss_qr_bill"><code>swiss_qr_bill</code></a> gems.</li>
</ul>
<hr />
<h2 id="happiness--wellbeing">Happiness & Wellbeing</h2>
<p>2024 was a year of personal growth. Balancing professional achievements with time for rest, travel, reflection, and relationships has been key.</p>
<p>I’m proud I found a balance that works for me. Not from work, but from the connections, experiences, and new friends I made along the way.</p>
<hr />
<h2 id="looking-ahead-to-2025">Looking Ahead to 2025</h2>
<p>As I look into 2025, my aim is to build on the progress and growth of the past few years. I want to stay thoughtful, focused, and reflective in my efforts. Here are a few key areas I plan to concentrate on:</p>
<p><strong>Writing & Blogging</strong></p>
<p>In 2025, I want to focus more on writing long-form technical content. While Hotwire Weekly has been a great start, blogging feels like an opportunity and great challenge to dive deeper into topics and communicate ideas more clearly and effectively.</p>
<p>I plan to use my blog to document:</p>
<ul>
<li>Patterns, techniques, and tools from my day-to-day work.</li>
<li>Insights and lessons learned from my Open Source work.</li>
<li>Reflections on the Ruby and Rails communities, including ideas for improving developer tooling, framework governance, and community-driven efforts.</li>
</ul>
<p>The goal is to share content in a more structured and thoughtful way, beyond quick social media posts.</p>
<p>I hope that these posts will contribute to the community while improving my ability to explain complex concepts. I want to step out of my comfort zone once again. Blogging is also something I’m not super comfortable with yet, which is why I want to start working on it.</p>
<p><strong>Talks & Public Speaking</strong></p>
<p>2024 was a turning point for me in public speaking, and while I’ve grown more comfortable with it, I know there’s a lot to improve. In 2025, I want to reflect on my previous talks and try to learn from them. It’s something I’ve been avoiding for a long time. But now I want to start tackling that.</p>
<p>I will be submitting proposals to conferences in 2025. I also want to give talks about the non-Hotwire topics I’ve been focusing on.</p>
<p>These events have been an incredible experience and an awesome opportunity for me which inspires me to keep going. They’ve become a significant part of my life and I want to keep that energy going in 2025.</p>
<p><strong>Open Source</strong></p>
<p>Open Source will remain an important piece of my work in 2025. A few priorities include:</p>
<ul>
<li>
<p><strong>Maintaining Core Projects</strong>: Continuing to contribute to core libraries like Stimulus, Stimulus Use, StimulusReflex, CableReady, and other libraries in the Hotwire ecosystem. Ensuring they remain robust, relevant, and maintained.</p>
</li>
<li>
<p><strong>Improving Tooling</strong>: Building on the groundwork laid in 2024 with <a href="https://github.com/marcoroth/stimulus-lsp"><strong>Stimulus LSP</strong></a>, <a href="https://github.com/marcoroth/turbo-lsp"><strong>Turbo LSP</strong></a> and the <a href="https://github.com/leonvogt/hotwire-dev-tools"><strong>Hotwire Dev Tools</strong> browser extension</a>. I want to explore how we can improve the developer experience for Rails and Hotwire developers even more.</p>
<p>This includes finishing and releasing my work-in-progress <strong>HTML-Aware ERB Parser</strong>, which can level up the developer experience even further in the above mentioned tools and beyond. The idea is to make writing, maintaining, and refactoring HTML+ERB code cleaner, more intuitive and more powerful — while benefiting the broader Ruby ecosystem.</p>
</li>
<li>
<p><strong>RubyVideo & RubyConferences</strong>: These community-driven projects have been incredibly rewarding, and I want to keep refining and growing them. I’m excited to see how they evolve in 2025.</p>
</li>
</ul>
<p>At the same time, I’ll be mindful of my own capacity and the importance of avoiding burnout. Open Source can sometimes feel like a cycle of constant expectations, especially if you do a lot of this Open Source work out of passion and as a “hobby”. I want to ensure I’m pacing myself while staying focused on the projects and ideas I truly care about.</p>
<p><strong>Community Engagement</strong></p>
<p>Being part of the Ruby and Rails communities has been one of the most fulfilling aspects of my career so far, and I’m committed to remaining an active voice in 2025.</p>
<p>I want to help the community thrive and make Ruby as appealing and accessible as possible. Because in the end Ruby is the language we all know and love.</p>
<p><strong>Personal Growth</strong></p>
<p>Lastly, 2025 will be about continuing to grow as a person, not just as a developer.</p>
<p>This past year has shown me that growth often comes from stepping outside my comfort zone. I plan to keep pushing myself in ways that feel meaningful, even if they’re sometimes uncomfortable.</p>
<hr />
<h2 id="conclusion">Conclusion</h2>
<p>To be blunt, 2024 has honestly been the best year of my life so far. I’ve never felt as happy, motivated, or fulfilled as I do now. I’m looking forward to carrying this momentum into 2025.</p>
<p>I’m particularly looking forward to improving my writing, continuing my work in Open Source, and staying open to new opportunities and collaborations. It’s exciting to think about the ways I can grow, both personally and technically.</p>
<hr />
<h2 id="github-sponsors">GitHub Sponsors</h2>
<p>Lastly, I wanted to give a huge thanks to all the people and organizations that supported me as a GitHub Sponsor, thank you so much for supporting my ideas and my vision by being a GitHub Sponsor!</p>
<div class="grid grid-cols-6 md:grid-cols-11 gap-2 mt-9">
<a href="https://github.com/itsameandrea" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/itsameandrea.png" alt="Andrea Rocca" data-controller="tooltip" title="Andrea Rocca" />
</a>
<a href="https://github.com/andrewmcodes" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/andrewmcodes.png" alt="Andrew Mason" data-controller="tooltip" title="Andrew Mason" />
</a>
<a href="https://github.com/avo-hq" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/avo-hq.png" alt="Avo HQ" data-controller="tooltip" title="Avo HQ" />
</a>
<a href="https://github.com/BaseSecrete" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/BaseSecrete.png" alt="Base Secrète" data-controller="tooltip" title="Base Secrète" />
</a>
<a href="https://github.com/c-sonnier" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/c-sonnier.png" alt="c-sonnier" data-controller="tooltip" title="c-sonnier" />
</a>
<a href="https://github.com/collindonnell" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/collindonnell.png" alt="Collin Donnell" data-controller="tooltip" title="Collin Donnell" />
</a>
<a href="https://github.com/davidteren" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/davidteren.png" alt="David Teren" data-controller="tooltip" title="David Teren" />
</a>
<a href="https://github.com/dmcge" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/dmcge.png" alt="dmcge" data-controller="tooltip" title="dmcge" />
</a>
<a href="https://github.com/DimaSamodurov" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/DimaSamodurov.png" alt="Dmytro Samodurov" data-controller="tooltip" title="Dmytro Samodurov" />
</a>
<a href="https://github.com/DRBragg" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/DRBragg.png" alt="Drew Bragg" data-controller="tooltip" title="Drew Bragg" />
</a>
<a href="https://github.com/swistaczek" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/swistaczek.png" alt="Ernest Bursa" data-controller="tooltip" title="Ernest Bursa" />
</a>
<a href="https://github.com/g-pavlik" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/g-pavlik.png" alt="Greg Pavlik" data-controller="tooltip" title="Greg Pavlik" />
</a>
<a href="https://github.com/irinanazarova" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/irinanazarova.png" alt="Irina Nazarova" data-controller="tooltip" title="Irina Nazarova" />
</a>
<a href="https://github.com/joemasilotti" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/joemasilotti.png" alt="Joe Masilotti" data-controller="tooltip" title="Joe Masilotti" />
</a>
<a href="https://github.com/karloscarweber" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/karloscarweber.png" alt="Karl Weber" data-controller="tooltip" title="Karl Weber" />
</a>
<a href="https://github.com/kirillplatonov" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/kirillplatonov.png" alt="Kirill Platonov" data-controller="tooltip" title="Kirill Platonov" />
</a>
<a href="https://github.com/leonvogt" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/leonvogt.png" alt="Leon Vogt" data-controller="tooltip" title="Leon Vogt" />
</a>
<a href="https://github.com/honestica" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/honestica.png" alt="Lifen" data-controller="tooltip" title="Lifen" />
</a>
<a href="https://github.com/lucianghinda" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/lucianghinda.png" alt="Lucian Ghinda" data-controller="tooltip" title="Lucian Ghinda" />
</a>
<a href="https://github.com/lxxxvi" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/lxxxvi.png" alt="Mario Schüttel" data-controller="tooltip" title="Mario Schüttel" />
</a>
<a href="https://github.com/omarluq" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/omarluq.png" alt="Omar Luq" data-controller="tooltip" title="Omar Luq" />
</a>
<a href="https://github.com/renuo" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/renuo.png" alt="Renuo AG" data-controller="tooltip" title="Renuo AG" />
</a>
<a href="https://github.com/rickbenavidez" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/rickbenavidez.png" alt="Rick Benavidez" data-controller="tooltip" title="Rick Benavidez" />
</a>
<a href="https://github.com/SethHorsley" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/SethHorsley.png" alt="Seth Horsley" data-controller="tooltip" title="Seth Horsley" />
</a>
<a href="https://github.com/sobstel" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/sobstel.png" alt="sobstel" data-controller="tooltip" title="sobstel" />
</a>
<a href="https://github.com/szTheory" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/szTheory.png" alt="szTheory" data-controller="tooltip" title="szTheory" />
</a>
<a href="https://github.com/trinitytakei" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/trinitytakei.png" alt="Trinity Takei" data-controller="tooltip" title="Trinity Takei" />
</a>
<a href="https://github.com/Uscreen-video" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/Uscreen-video.png" alt="Uscreen" data-controller="tooltip" title="Uscreen" />
</a>
<a href="https://github.com/valentinorusconi" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/valentinorusconi.png" alt="Valentino Rusconi" data-controller="tooltip" title="Valentino Rusconi" />
</a>
<a href="https://github.com/palkan" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/palkan.png" alt="Vladimir Dementyev" data-controller="tooltip" title="Vladimir Dementyev" />
</a>
<a href="https://github.com/wbotelhos" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/wbotelhos.png" alt="Washington Botelho" data-controller="tooltip" title="Washington Botelho" />
</a>
<a href="https://github.com/yshmarov" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/yshmarov.png" alt="Yaro Shm" data-controller="tooltip" title="Yaro Shm" />
</a>
<a href="https://github.com/zzak" class="not-prose">
<img loading="lazy" class="h-18 w-18 rounded-full border nocaption not-prose" nocaption="true" src="https://github.com/zzak.png" alt="zzak" data-controller="tooltip" title="zzak" />
</a>
</div>
<hr />
<p>Thank you for reading, I hope you enjoyed this post!</p>Marco RothSupercharge your Stimulus controllers with Custom APIs2023-07-27T03:00:00+00:002023-07-27T03:00:00+00:00repo://posts.collection/_posts/2023-07-27-supercharge-your-stimulus-controllers-with-custom-apis.md<p>Since its initial release in 2017, Stimulus has undergone minimal breaking changes and has been known for its stability. Even a Stimulus controller written for version 1.0.0 still works flawlessly with the latest 3.2.1 release, using the same syntax.</p>
<p>Considering the fast-paced world of JavaScript, this stability might seem unexpected. While Stimulus has added new features over the years, its primary strength lies in the various APIs introduced with each new release.</p>
<ul>
<li>Stimulus 1.0.0: <strong>Targets API</strong></li>
<li>Stimulus 2.0.0: <strong>Values API</strong> and the <strong>CSS classes API</strong></li>
<li>Stimulus 3.2.0: <strong>Outlets API</strong></li>
</ul>
<p>While also other features were added in these releases, it’s the Stimulus APIs that truly make Stimulus and the releases special.</p>
<p>In this article, we will explore the process of creating a new Stimulus API that can be used in any Stimulus Controller.</p>
<h3 id="the-stimulus-elements-api">The Stimulus Elements API</h3>
<p>Most Stimulus controllers should ideally be designed to be used on multiple DOM elements. However, there are also cases where we need to reference elements outside of the controller’s scope or cases where you design a controller which is meant to control a specific element on the page.</p>
<p>The use of the Targets and/or Outlets API also might seem logical for these cases, but the Elements API might better suited if …</p>
<ul>
<li>… you either can’t control the elements you want to reference and/or can’t define them as targets</li>
<li>… they are outside of the controller scope and you don’t need them to be full-blown outlets</li>
<li>… or the controller specifically needs to control a special element on the page, independent from it’s own controller element and it’s children</li>
</ul>
<p>A common pattern emerges in such situations: defining a <code>get</code> function on the controller with names like <code>[name]Element</code> or <code>[name]Elements</code>. These functions return a single element or a set of elements using <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector"><code>document.querySelector(...)</code></a> or <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll"><code>document.querySelectorAll(...)</code></a> respectively.</p>
<pre><code class="language-js">import { Controller } from "@hotwired/stimulus"
import tippy from "tippy.js"
export default class extends Controller {
connect() {
this.backdropElement.classList.remove("hidden")
this.itemElements.forEach(element => ...)
this.tippyElements.forEach(element => tippy(element))
}
// ...
get backdropElement() { // [tl! highlight:start]
return document.querySelector("#backdrop")
}
get itemElements() {
return document.querySelectorAll(".item")
}
get tippyElements() {
return document.querySelectorAll("[data-tippy]")
} // [tl! highlight:end]
}
</code></pre>
<p>To avoid repetition and improve code readability, we will build an API that abstracts this pattern into a Stimulus API, similar to the Targets or Outlets API.</p>
<h3 id="creating-the-elements-api">Creating the Elements API</h3>
<p>Stimulus internally uses so-called <code>Blessings</code>, which enhance the <code>Controller</code> class with new functionality. Each API is a separate <code>Blessing</code>, maintaining a modular design.</p>
<p>The Stimulus <code>Controller</code> class has a <code>static blessings</code> Array which holds all of the <code>Blessings</code> the controller should be blessed with.</p>
<p>In Stimulus 3.2, the <code>blessings</code> array contains:</p>
<pre><code class="language-js">// @hotwired/stimulus - src/core/controller.ts
export class Controller {
static blessings = [ // [tl! highlight:start]
ClassPropertiesBlessing,
TargetPropertiesBlessing,
ValuePropertiesBlessing,
OutletPropertiesBlessing,
] // [tl! highlight:end]
// ...
}
</code></pre>
<p>With that knowledge we can create our own <code>ElementPropertiesBlessing</code> and extend Stimulus with our Elements API by adding it to the <code>blessings</code> array.</p>
<h3 id="proposed-api">Proposed API</h3>
<p>Inspired by other Stimulus APIs, we’ll declare a <code>static</code> property called <code>elements</code>, defining the elements we want to reference along with their CSS selectors.</p>
<pre><code class="language-js">import { Controller } from "@hotwired/stimulus"
import tippy from "tippy.js"
export default class extends Controller {
static elements = { // [tl! focus:start]
backdrop: "#backdrop",
item: ".item",
tippy: "[data-tippy]"
} // [tl! focus:end]
connect() {
this.backdropElement.classList.remove("hidden")
this.itemElements.forEach(element => ...)
this.tippyElements.forEach(element => tippy(element))
}
// ...
}
</code></pre>
<p>By calling <code>[name]Element</code> or <code>[name]Elements</code>, the API will determine whether to use <code>document.querySelector(...)</code> or <code>document.querySelectorAll(...)</code>, streamlining the process.</p>
<h3 id="implementing-the-elementpropertiesblessing">Implementing the <code>ElementPropertiesBlessing</code></h3>
<p>Let’s dive into creating the <code>ElementPropertiesBlessing</code> function that will power our Elements API. We’ll create a new file called <code>element_properties.js</code> exporting the <code>ElementPropertiesBlessing</code> constant:</p>
<pre><code class="language-js">// app/javascript/element_properties.js
export function ElementPropertiesBlessing(constructor) {
const properties = {}
return properties
}
</code></pre>
<p>The <code>ElementPropertiesBlessing</code> function takes the <code>constructor</code> (controller) as an argument. Inside this function, we initialize an empty <code>properties</code> object to store the properties which should get added to the <code>constructor</code>.</p>
<p>In context of our Elements API we want <code>properties</code> to contain the functions for our <code>[name]Element</code> and <code>[name]Elements</code> getters, which should look something like in the end:</p>
<pre><code class="language-js">{
'backdropElement': {
get() {
return document.querySelector(...)
}
},
'backdropElements': {
get() {
return document.querySelectorAll(...)
}
},
'itemElement': { get() { /* ... */ } },
'itemElements': { get() { /* ... */ } },
'tippyElement': { get() { /* ... */ ) },
'tippyElements': { get() { /* ... */ } }
}
</code></pre>
<p>In order to construct that object we want to read the <code>static elements</code> property of the controller.</p>
<p>Stimulus (privately) exposes two functions we can use for this. Depending on the structure of the definition we can either use <code>readInheritableStaticArrayValues()</code> for an Array structure (like the Targets API) or <code>readInheritableStaticObjectPairs()</code> for an Object structure (like the Values API).</p>
<details style="background: #ececec; padding: 10px; border-radius: 10px;">
<summary>Using privately exposed Stimulus functions</summary><br />
The next version of Stimulus makes it easier to access parts of the private API thanks to my <a href="https://github.com/hotwired/stimulus/pull/686">pull request</a>.<br /><br />
If you want to make use of that today, you can install the latest dev-build using:
<pre>
yarn add @hotwired/stimulus@https://github.com/hotwired/dev-builds/archive/@hotwired/stimulus/7b810ec.tar.gz
</pre>
Otherwise, you need to wait for the upcoming Stimulus 3.3 release.
</details>
<p>Since our Elements API uses an object to define the elements and selectors we want to use the <code>readInheritableStaticObjectPairs()</code> function.</p>
<pre><code class="language-js">import { readInheritableStaticObjectPairs } from "@hotwired/stimulus/dist/core/inheritable_statics" // [tl! focus]
export function ElementPropertiesBlessing(constructor) {
const properties = {}
const definitions = readInheritableStaticObjectPairs(constructor, "elements") // [tl! focus]
return properties
}
</code></pre>
<p>The <code>definitions</code> variable now holds an array of definitions, which looks like this:</p>
<pre><code class="language-js">[
["backdrop", "#backdrop"],
["item", ".item"],
["tippy", "[data-tippy]"]
]
</code></pre>
<p>Let’s define a <code>propertiesForElementDefinition()</code> function, which generates the corresponding properties for each element definition.</p>
<pre><code class="language-js">import { readInheritableStaticObjectPairs } from "@hotwired/stimulus/dist/core/inheritable_statics"
import { namespaceCamelize } from "@hotwired/stimulus/dist/core/string_helpers" // [tl! foucs]
export function ElementPropertiesBlessing(constructor) {
const properties = {}
const definitions = readInheritableStaticObjectPairs(constructor, "elements")
return properties
}
// [tl! focus:start]
function propertiesForElementDefinition(definition) {
const [name, selector] = definition
const camelizedName = namespaceCamelize(name)
return {
[`${camelizedName}Element`]: {
get() {
return document.querySelector(selector)
}
},
[`${camelizedName}Elements`]: {
get() {
return document.querySelectorAll(selector)
}
}
}
}
// [tl! focus:end]
</code></pre>
<p>In this function, we extract the <code>name</code> and <code>selector</code> from the definition array. Then, using <code>namespaceCamelize()</code>, we create camel-cased versions of the element names.</p>
<p>The <code>propertiesForElementDefinition()</code> function returns an object containing two properties for each element definition: <code>[name]Element</code> and <code>[name]Elements</code>. The former returns a single element using <code>document.querySelector(...)</code>, while the latter returns a <code>NodeList</code> using <code>document.querySelectorAll(...)</code>.</p>
<p>For each element definition in the <code>definitions</code> array, we want to call the <code>propertiesForElementDefinition()</code> function to create the corresponding properties and merge them into the <code>properties</code> object using <code>Object.assign()</code>. Finally, we return the <code>properties</code> object containing all the element properties for the given controller.</p>
<pre><code class="language-js">export function ElementPropertiesBlessing(constructor) {
const properties = {}
const definitions = readInheritableStaticObjectPairs(constructor, "elements")
// [tl! focus:start]
definitions.forEach(definition => {
Object.assign(properties, propertiesForElementDefinition(definition))
})
// [tl! focus:end]
return properties
}
</code></pre>
<p>By implementing the <code>ElementPropertiesBlessing</code>, we have constructed the backbone of our Elements API. This blessing will dynamically add the necessary properties to each controller instance, enabling easy access to the referenced DOM elements defined in the static <code>elements</code> property.</p>
<h3 id="installing-the-api">Installing the API</h3>
<p>To apply our new API, we need to tell Stimulus about it when starting the application. In Rails apps this is typically done in <code>app/javascript/controllers/application.js</code>.</p>
<p>We want to add our <code>ElementPropertiesBlessing</code> to the <code>blessings</code> array on the <code>Controller</code> class. By importing both <code>Controller</code> and <code>ElementPropertiesBlessing</code> we can push the function into the <code>blessings</code> array and Stimulus will add the properties to each new controller instance.</p>
<pre><code class="language-js">// app/javascript/controllers/application.js
import { Application } from "@hotwired/stimulus"
import { Controller } from "@hotwired/stimulus" // [tl! focus:start]
import { ElementPropertiesBlessing } from "../element_properties"
Controller.blessings.push(ElementPropertiesBlessing) // [tl! focus:end]
const application = Application.start()
// Configure Stimulus development experience
application.warnings = true
application.debug = false
window.Stimulus = application
export { application }
</code></pre>
<p>With this, the Elements API is now available in every Stimulus controller within our application.</p>
<h3 id="more-ideas">More ideas</h3>
<p>We could extend the API even more and define a <code>has[Name]Element</code> property which would check if an element exists for a given selector, similar to the Targets and Outlets API.</p>
<p>We could also think about extending the API to accept the selectors for the <code>elements</code> on the controller element using data attributes in the format of <code>data-[identifier]-[element]-element</code>. This would allow to override the selectors for the elements defined in the <code>elements</code> object:</p>
<pre><code class="language-html"><div
data-controller="test"
data-test-backdrop-element=".backdrop"
data-test-item-element=".item:not([data-disabled])"
data-test-tippy-element="span.tippy"
></div>
</code></pre>
<h3 id="conclusion">Conclusion</h3>
<p>We successfully built a new Stimulus “Elements API,” abstracting the pattern of referencing elements in controllers using CSS selectors. By leveraging Stimulus’ modular architecture and the concept of Blessings, we extended the Controller class with our own API.</p>
<p>The Elements API enhances code readability by encapsulating the element lookup logic and reducing the need for repetitive code in controllers. It improves productivity, making it more elegant and efficient to work with DOM elements in various use cases.</p>
<p>Part of the reason for this post was to demonstrate that not every new API needs to ship with Stimulus itself to be useful in applications. Shipping custom APIs as application-specific code allows us build APIs to our specific needs without polluting the upstream framework with APIs not everyone might benefit from.</p>
<p>With that being said, I could also see a future where we ship ready-made <code>Blessings</code> and the Elements API could become an optional part of Stimulus itself or part of a package like <a href="https://github.com/stimulus-use/stimulus-use"><code>stimulus-use</code></a>. These APIs wouldn’t be enabled by default, but you could opt-in to enable the APIs you like to use in your application.</p>
<p>Feel free to experiment with my Elements API or potential new APIs you come up with and let me know your thoughts on <a href="https://twitter.com/marcoroth_">Twitter</a> or <a href="https://ruby.social/@marcoroth">Mastodon</a>.</p>
<p>I would love to see what you come up with.</p>
<p>Happy coding!</p>Marco Roth