GretelX
One class of plain Ruby methods per namespace. A view that names no crumb gets the trail of its action. The trail comes back as data, and your own component renders it.
gem "gretelx"
Why
gretel works. But on a real back office (one file of about 1,000 lines, 265 crumbs, 380 call sites), its shape is what hurts:
- one global file and one flat namespace, where a crumb defined twice silently replaces the first, and an admin area cannot keep its breadcrumbs apart from a storefront’s;
- mistakes that only show when someone opens the page;
- blocks run through
instance_exec, and helpers reached throughmethod_missing; - every trail written by hand, even when the routes already describe it.
GretelX keeps gretel’s good idea, a crumb names its parent, and changes the rest.
What changes
One class per namespace
Admin::Breadcrumbs and Storefront::Breadcrumbs each hold their own crumbs, under the same names if they like. Zeitwerk loads them, so they load before a prefork server forks.
One crumb per Ruby method
The parent is a plain method call, and the URL helpers come from your routes. No DSL, no method_missing, and a backtrace points at the method.
Derived by default
A view that names no crumb gets the trail of its action, built from the routes and the record its controller already loaded, nested routes included.
Mistakes raise early
A crumb defined twice raises when the class loads. An unknown crumb raises in development and test; in production it is reported and the page renders without a trail.
Checked in tests
One assert_breadcrumbs_valid reads every crumb with Prism: unknown parents and route helpers, view calls to missing crumbs, and dead crumbs fail the test, without loading a record.
Data, not HTML
breadcrumb_trail returns levels (label, URL, extra links, current?). No styles, no renderer options: your component decides the markup.
JSON-LD for storefronts
breadcrumb_json_ld renders the same trail as a schema.org BreadcrumbList; a relative URL raises in development and test, before Google ever reads it.
A codemod from gretel
gretelx-convert turns config/breadcrumbs.rb into a trail class, and a test compares the trails both gems draw before gretel goes.
Five cases, side by side
gretel on the left, in config/breadcrumbs.rb; GretelX on the right, in app/breadcrumbs/admin/breadcrumbs.rb.
A nested resource
gretel
crumb :order_messages do |order|
link "Messages", admin_order_messages_path(order)
parent :order, order
end
GretelX
def order_messages(order)
crumb "Messages", admin_order_messages_path(order),
parent: order(order)
end
A polymorphic parent
gretel
crumb :returns do |owner|
if owner
link "Returns", [:admin, owner, :returns]
parent owner
else
link "Returns", admin_returns_path
end
end
GretelX
def returns(owner = nil)
return crumb("Returns", admin_returns_path) unless owner
crumb "Returns", [:admin, owner, :returns],
parent: crumb_for(owner)
end
A label computed from the record
gretel
crumb :order do |order|
link "Order #{order.number}", admin_order_path(order)
parent order.customer ? [:customer_orders, order.customer] : :orders
end
GretelX
def order(order)
crumb "Order #{order.number}", admin_order_path(order),
parent: order.customer ? customer_orders(order.customer) : orders
end
A recursive tree
The ancestors are loaded once, in one query, and handed down the recursion.
gretel
crumb :category do |category, ancestors = category.ancestors.to_a|
link category.name, admin_category_path(category)
*grandparents, parent_category = ancestors
parent_category ? parent(:category, parent_category, grandparents) : parent(:categories)
end
GretelX
def category(category, ancestors = category.ancestors.to_a)
*grandparents, parent_category = ancestors
crumb category.name, admin_category_path(category),
parent: parent_category ? category(parent_category, grandparents) : categories
end
A form
gretel: one generic crumb, called from each new and edit view
crumb :generic_form do |model|
if model.new_record?
link "New", nil
parent model.class.model_name.plural.to_sym
else
link "Edit", nil
parent model
end
end
<%# app/views/admin/posts/edit.html.erb %>
<% breadcrumb :generic_form, @post %>
GretelX
# Nothing to write: new and edit derive
# "New" under the collection,
# "Edit" under the record.
Quick start
app/breadcrumbs/admin/breadcrumbs.rb
module Admin
class Breadcrumbs < GretelX::Trail
def root
crumb "Home", admin_root_path
end
def orders
crumb "Orders", admin_orders_path
end
def order(order)
crumb order.to_s, admin_order_path(order), parent: orders
end
end
end
app/views/layouts/admin.html.erb
<nav aria-label="Breadcrumb">
<ol>
<% breadcrumb_trail.each do |level| %>
<li><%= link_to_unless level.current?, level.label, level.url %></li>
<% end %>
</ol>
</nav>