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.
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.
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”.
Herb v0.4.0: Linter, Formatter (Preview), and a Big Step Forward
The first headline from the talk is the release of Herb v0.4.0, and with it, the first versions of the Herb Linter and a first preview version of the Herb Formatter.
The most notable things in the release are:
- Herb Linter
- Herb Formatter (Preview)
- Improved Language Server
- Improved Visual Studio Code Extension
- Parser Bug Fixes and compatibility improvements
You can check the full Herb v0.4.0 changelog on GitHub.
These new tools are all built on top of the Herb Parser, a new fault-tolerant, HTML-aware ERB parser written in C, which I introduced at RubyKaigi earlier this year. These tools aim to bring the modern developer experience we expect to *.html.erb
files.
But more importantly, they’re fully integrated into the Herb Language Server and the improved Visual Studio Code extension, which now:
- supports linting and auto-formatting directly in the editor
- offers whole-project analysis
- includes a new sidebar panel for diagnostics and the analysis results
You can use the linter and formatter via the CLI or in-editor.
Herb Linter
The Ruby Style Guide provides recommendations and best practices for Ruby code. Linters like RuboCop and Standard make this actionable by implementing and enforcing these guidelines.
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.
CLI
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.
At the end of the run, you’ll get a summary of the most violated rules:
For lots of offenses, use the --simple
flag for more compact output:
Language Server Integration
While a good CLI is important, catching offenses directly in your editor provides an even tighter feedback loop:
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:
Clicking on a rule ID will open the documentation for that specific linter rule:
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:
We also run the Linter over all code blocks in the documentation and annotate the examples using Shiki and Twoslash.
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.
More Linter Rules and Feedback
You can check out the full list of already implemented linter rules in the documentation. There are also a lot more rules to be implemented if you are looking for a way to contribute to the project.
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.
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.
Herb Formatter (Preview)
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.
Please, feel free to report any case the formatter formats the document in a weird or unpredictable way.
Format on Save
The Herb Formatter is also fully integrated into the Herb Language Server but is disabled by default for the time being.
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:
Once enabled it will format your files on save as long as there is no error present in the document:
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!
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.
A Vision For The Rails View Layer
Context
Action View has been part of Rails since it initially released in July 2004 with Rails 0.5.0.
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. From the original announcement
ERB (Embedded Ruby) has been the default since the first release and still is today.
Here are some of the big changes in Action View over the last 20 years:
- 2005 – Rails 1.0 – Layouts + Partials
- 2007 – Rails 2.0 –
*.html.erb
(show.html.erb
instead ofshow.rhtml
) - 2007 – Rails 2.3 –
render_component
deprecated -
2010 – Rails 3.0 – Automatic HTML escaping
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
h
helper to escape user-provided content. -
2010 – Rails 3.0 –
ActionView::Template
The Rails-Merb merge brought some internal improvements to ActionView. Templates were now handled by an
ActionView::Template
abstraction with a unified interface for different handlers like ERB, Builder, etc. -
2013 – Rails 4.0 – Russian Doll Caching
Tracks template dependencies with digest keys
-
2014 – Rails 4.1 – Action View Variants
actionview
is extracted as it’s own gem - 2017 – Rails 5.1 –
form_with
-
2020 – Rails 6.1 (Beta) –
ActionView::Component
While
ActionView::Component
didn’t make it into Rails 6.1 but eventually became ViewComponent. -
2020 – Rails 6.1 –
render_in(context)
Even though
ActionView::Component
didn’t make it into Rails we got the newrender_in
API that allows pretty much any Ruby object to be rendered by the Railsrender
method as long as it implements therender_in
interface. - 2021 – Rails 7.0 – Hotwire by default
- 2023 – Rails 7.1 – Partial Strict Locals
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.
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.
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.
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.
But this server-side rendered approach doesn’t come without its issues:
Some of these problems are solved with tools like ViewComponent, nice_partials or other similar tools, but there’s still a lot left to be solved, especially in the developer tooling space.
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?
A new ERB Rendering Engine
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.
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.
Currently all ERB rendering engines treat the non-Ruby code as Strings, making them a String Templating Language. But what we want, ideally, is an HTML Templating Language: an engine that cannot produce invalid HTML, because the engine is aware of the HTML structure around the ERB tags in the template.
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”:
ReActionView
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.
It’s important for me to note 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.
But with that out of the way, let me walk you through the six adoption levels.
Level 1: Better Feedback and Developer Experience
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.
This works great if the exception happens in an .rb
file. Sadly it doesn’t work as nicely if you have syntax errors in *.html.erb
files.
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.
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.
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.
On this level Herb acts as a parser and analyzer. It catches common issues in real time.
Level 2: HTML-aware ERB rendering Engine
On this level, we would take the ERB rendering engine from being a String Template Engine to an actual HTML Template Engine. The idea is to keep everything backwards compatible, as long as the current *.html.erb
view files already contain and render valid HTML.
Instead of using ERB’s default engine, templates could be rendered using a new Herb rendering engine.
Part of the inspiration of ReActionView and the actual project name “Herb” comes from Elixir and their way of approaching templating languages.
Elixir has an ERB equivalent called EEx
, 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.
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 H from HTML and the ERB, following the naming scheme of HEEx.
Let’s look at an example how a String Template Engine would compare to a HTML Template Engine. Given the following ERB template (which has a missing </h1>
closing tag):
<h1>Hello, <%= name %>!
and ask Action View to compile it using:
template = ActionView::Template.new( "<h1>Hello, <%= name %>!", "test.html.erb", ActionView::Template::Handlers::ERB, locals: [:name]) compiled = template.handler.call( template, template.source)
it will happily compile the template. Action View is only concerned about turning the given template into compiled Ruby code that concatenates strings:
Whereas, if we would build a new .herb
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.
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.
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.
Level 3: Action View Optimizations
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.
John Hawthorn did some similar work on actionview_precompiler
(and dynamic_locals
) that is very interesting and might be something we could use as a starting point or improve upon.
Another possible optimization: inlining <%= render partial: "post" %>
calls. This would eliminate runtime partial lookups by directly embedding the partial content into the compiled view.
Instead of compiling to @output_buffer.append=( render partial: 'post' );
, we would read and inline the partial at compile time.
This requires confidence in identifying the right partial, which is why our linter rules encourage explicit render arguments. Worst case: we fall back to current behavior. Best case: we eliminate runtime lookups.
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.
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.
Level 4: Reactive ERB Templates
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”.
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.
Think of this as Phoenix LiveView HEEx-like updates, but still keeping it Rails-y by using the existing .html.erb
view files.
If you are not familiar with Phoenix LiveView, you can also think of this as the ERB engine emitting a set of <turbo-stream>
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.
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.
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.
Opt-In Syntax and Strict Mode
While we could make these reactive ERB templates work with regular *.html.erb
files, there is also an opportunity to introduce a new *.html.herb
file-ending that would have this behavior turned on by default.
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 *.html.erb
. You would only use and rename existing views to *.html.herb
where it’s needed and makes sense.
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.
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 Rux/JSX) could prove to become very handy.
class NameComponent < ApplicationComponent def initialize(first_name:, last_name:) @first_name = first_name @last_name = last_name endend
and render it using:
<NameComponent first-name="Rails" last-name="Conf" />
Another thing we could enforce using a new file-ending with stricter rules would be things like:
- Check for invalid syntax
- Check for valid HTML5 (via Nokogiri)
- Accessibility/A11y Checks (i.e. missing
alt
attributes on<img>
tags) - Cross-site scripting/XSS Checks (i.e no unsafe ERB interpolation)
- Only one element with the same ID
- …
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.
Level 5: Universal client-side Templates
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.
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”.
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.
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.
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.
Level 6: External Components
And finally, we explore the ability to mount external UI components (like React, Vue, Svelte, …) directly within *.html.herb
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.
The idea is to have a initializer file, similar to how importmap-rails
does:
In that file (like config/initializers/reactionview.rb
) you would be able to register existing components from NPM packages, or tell it to import components from a certain directory within your app:
With the components registered, they will be automatically available within the context of *.html.herb
files:
This approach is less invasive compared to Inertia.js Rails which requires you to render a whole page with Inertia.
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.
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.
This approach is very similar to Turbo Mount, 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.
How It All Fits Together
Today’s tools (the linter and formatter) make .html.erb
safer and easier to work with. But they’re also building blocks for tomorrow’s ReActionView.
The progression is natural:
- Parser understands HTML+ERB structure
- Linter/Formatter enforces best practices and consistency
- HTML-aware rendering prevents invalid markup at compile time
- Reactive templates efficiently update only what changes
- Universal templates work on both server and client
It’s a roadmap designed for gradual, non-disruptive adoption that can be progressively enhanced where needed that feels Rails-like.
Help Improve the Herb Parser
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.
Simply run these commands in your project:
cd your_project/ gem install herb herb analyze . # analyze all *.html.erb in the current directoryherb analyze app/views # or just inside a specific directory
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.
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 .log
file which will have more details on why and how it failed.
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.
Conclusion
Back in April, Herb initially only released with the parser, but now it’s become a more complete toolkit for HTML+ERB files.
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.
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.
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.
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.
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.
If you missed the talk, you can find the slides on Speakerdeck. In the meantime, I’d love for you to try the tools, file issues, and join the conversation.
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.
Thank you!
- Marco
Code blocks highlighted by Torchlight.