This library uses Ruby blocks (closures) to dynamically generate HTML, providing flexible DSL approach to building HTML documents. Its ability to work with custom helper methods (executed in a different scope) adds to its extensibility and modularity.
HTMG is around 5x times faster than ERB. See rspec/benchmark.rb.
user system total real
HTMG with Data: 0.371457 0.000961 0.372418 ( 0.372436)
ERB with Data: 1.874767 0.010206 1.884973 ( 1.884978)
This speed advantage is due to HTMG functional, stateless approach, which directly generates HTML using Ruby blocks and method_missing without the need for template parsing or compilation.
There are plenty of alternative html builders. This one uses on speed, simplicity and LLM frienliness. It is about 100 lines of code. It makes html tags into closures (ruby blocks) that you can nest, combine and test easily. This library uses Ruby functional language features so no classes/objects, no state, no overhead and no uncertainty as to the outcome.
Much development is done with coding assistants. Here is what one LLM has to say about HTMG vs ERB:
HTMG (Ruby Blocks) is Significantly Easier for LLMs to Reason About - here's why:
1 Explicit Code Flow
HTMG shows direct Ruby execution:
htmg do
div(class: "alert") do
h3 { "Todo Updated!" }
p { "ID: #{todo.id}" }
end
end
vs ERB's implicit context:
<div class="alert">
<h3><%= @todo.title %></h3>
<% if @todo.due_date? %>
<p>Due: <%= fmt_date(@todo.due_date) %></p>
<% end %>
</div>
2 Type-Safe Analysis
HTMG's structure allows strict validation:
# LLM can reason about method arguments
def todo_item(todo)
li(data: { id: todo.id }) { todo.title } # Clearly typed
end
vs templating's string-based ambiguity:
<li data-id="<%= todo[:id] %>"><%= todo.title.upcase %></li>
3 Direct Ruby Integration
No context switching between languages:
# Full Ruby power in components
def todo_list(todos)
if todos.empty?
render_empty_state
else
ul { todos.map { |t| todo_item(t) } }
end
end
4 Predictable Output
HTMG's closure structure guarantees valid HTML nesting:
table do
thead { tr { th "Title" } }
tbody do
@items.each { |i| tr { td i.name } }
end
end
vs templating's risk of unclosed tags:
<% @items.each do |i| %>
<tr><td><%= i.name %>
<% end %>
5 Better Error Tracing
Line numbers map 1:1 with Ruby code vs template compilation steps.
Benchmark Example
Task HTMG Accuracy ERB Accuracy
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Add conditional CSS 98% 73%
Fix nested tags 100% 82%
Add data attributes 96% 68%
Why This Matters for Your Project
With HTMG:
1 I can guarantee valid HTML structure through code analysis
2 Refactoring becomes method-based vs text manipulation
3 Components are truly reusable Ruby objects
4 No hidden template context variables
You can use the htmg gem in a Sinatra application by defining a layout and individual views. Here are some approaches:
Create a layout method that uses htmg to generate the common structure of your HTML pages. This method can be used to wrap individual view methods.
# layout.rb
module LayoutHelper
include HTMG
def layout(title:, &block)
html5 do
head {
title { title } +
body {
header { h1 { a(href: "/") { "My Site" } } } +
main(&block) +
footer do
small {
[ a(href: "/"){ "Home" },
a(href: "/about") { "About" }
].join(" ")
}
end
}
}
end
end
endDefine individual methods for each view, using htmg to generate the specific content for each page.
# views.rb
module Views
include HTMG
def home_view
h2 { "Welcome to My Site" } +
p { "This is the home page." }
end
def about_view
h2 { "About Us" } +
p { "We are a company that does things." }
end
endIntegrate the layout and view methods into your Sinatra routes. There are two approaches depending on whether you're using traditional or modular Sinatra apps.
# app.rb
require 'sinatra'
require 'htmg'
module LayoutHelper
include HTMG
def layout(title:, &block)
html5 do
head {
title { title } +
body {
header { h1 { a(href: "/") { "My Site" } } } +
main(&block) +
footer do
small {
[ a(href: "/"){ "Home" },
a(href: "/about") { "About" }
].join(" ")
}
end
}
}
end
end
end
module Views
include HTMG
def home_view
h2 { "Welcome to My Site" } +
p { "This is the home page." }
end
def about_view
h2 { "About Us" } +
p { "We are a company that does things." }
end
end
include Views
include LayoutHelper
include HTMG
get '/' do
htmg do
layout(title: "Home") { home_view }
end
end
get '/about' do
htmg do
layout(title: "About") { about_view }
end
endFor modular Sinatra apps, you need to explicitly include HTMG in each helper module and use helpers instead of include:
# app.rb
require 'sinatra/base'
require 'htmg'
class MyApp < Sinatra::Base
module LayoutHelper
include HTMG
def layout(title:, &block)
html5 do
head {
title { title } +
body {
header { h1 { a(href: "/") { "My Site" } } } +
main(&block) +
footer do
small {
[ a(href: "/"){ "Home" },
a(href: "/about") { "About" }
].join(" ")
}
end
}
}
end
end
end
module Views
include HTMG
def home_view
h2 { "Welcome to My Site" } +
p { "This is the home page." }
end
def about_view
h2 { "About Us" } +
p { "We are a company that does things." }
end
end
helpers LayoutHelper
helpers Views
include HTMG
get '/' do
htmg do
layout(title: "Home") { home_view }
end
end
get '/about' do
htmg do
layout(title: "About") { about_view }
end
end
run! if app_file == $0
endThe key differences are:
- Use
Sinatra::Baseand create a class - Each helper module must include HTMG
- Use
helpersinstead ofinclude - Add
run!for standalone execution
These examples demonstrate how you can structure your Sinatra application to use the htmg gem for generating HTML content. You can define a common layout and individual views, then use them in your Sinatra routes to render complete pages.
require 'htmg'
include HTMG
# Simple element
puts htmg { div { "Hello World" } }
# => <div>Hello World</div>
# With attributes
puts htmg { a(href: "https://example.com") { "Click me" } }
# => <a href="https://example.com">Click me</a>
# Nested elements
puts htmg { div(class: "container") { span { "Nested content" } } }
# => <div class="container"><span>Nested content</span></div>module LayoutHelper
include HTMG
def full_page(title:)
htmg do |scope|
html5 do
head {
meta(charset: "utf-8") +
title { title }
} +
body {
header { scope.navigation } +
main { scope.content(title) } +
footer { scope.footer_content }
}
end
end
end
def navigation
htmg do
nav(class: "main-nav") {
ul {
%w[Home About Contact].map { |item|
li { a(href: "/#{item.downcase}") { item } }
}.join
}
}
end
end
def content(title)
htmg do
article {
h1 { title } +
section(class: "content") { "Main article content" }
}
end
end
def footer_content
htmg do
div(class: "footer") {
"© #{Time.now.year} My Company"
}
end
end
endputs htmg { html5 { body { "Content" } } }
# => <!DOCTYPE html><html><body>Content</body></html># Image tag helper
def img_tag(src, alt: "", **attrs)
htmg { img(src: src, alt: alt, **attrs) }
end
# Form input helper
def input_field(type:, name:, **attrs)
htmg { input(type: type, name: name, **attrs) }
end
# Table helper
def table(data, **attrs)
htmg do
table(**attrs) {
thead {
tr {
data.first.keys.map { |header| th { header.to_s.capitalize } }.join
}
} +
tbody {
data.map { |row|
tr {
row.values.map { |value| td { value.to_s } }.join
}
}.join
}
}
end
end# Using environment variable
ENV['HTMG_EXTRA_TAGS'] = 'custom-tag,another-tag'
# Using constant
HTMG::EXTRA_TAGS = [:custom1, :custom2]
puts htmg { custom1 { "Custom content" } }
# => <custom1>Custom content</custom1># Unescaped content (default)
puts htmg { div { "<script>alert('hi')</script>" } }
# => <div><script>alert('hi')</script></div>
# Escaped content
puts htmg { div { h("<script>alert('hi')</script>") } }
# => <div><script>alert('hi')</script></div>When you call htmg method with a block, evrything inside this block is either a string or a method/function that returns a string. Therefore you should join them with a + sign or wrap them into an array and join, as shown above.
Inside the htmg block you can enter any standard html5 element and it will be treated as a function. If you need to use some non-standard tags you can add them to HTMG::EXTRA_TAGS constant or to the HTMG_EXTRA_TAGS environment variable, separated by comma.
Notice an optional scope block argument. It allows access the parent scope and call methods outside the htmg block.
Have a look at spec directory to see more examples.
TODO: Replace UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
Install the gem and add to the application's Gemfile by executing:
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORGIf bundler is not being used to manage dependencies, install the gem by executing:
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORGTODO: Write usage instructions here
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/1gor/htmg.
The gem is available as open source under the terms of the MIT License.