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 through method_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.

Namespaces

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.

Usage

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.

Derived trails

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.

Errors

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.

Checked in tests

Data, not HTML

breadcrumb_trail returns levels (label, URL, extra links, current?). No styles, no renderer options: your component decides the markup.

Rendering

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.

Storefronts

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.

From gretel

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>