Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

43 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Breadcrumb-Buddy

Breadcrumb-Buddy Logo

Breadcrumb-Buddy is a lightweight ColdBox module that simplifies breadcrumb navigation for your web applications. It generates dynamic breadcrumbs based on ColdBox events, supports aliases for intuitive usage, and allows recursive entity hierarchies (e.g., nested pages). It's plug-and-play with minimal setup, perfect for blogs, CMS, or any app needing clear navigation trails.

This module was inspired from the very well thought out Laravel-Breadcrumbs package. This module is not a port of that package. It was designed to bring similar breadcrumb functionality to ColdBox.

Requirements

  • ColdBox: 8.0+
  • CFML Engines:
    • ACF 2023+
    • Lucee 5.3+
    • Boxlang 1.0.0+

Features

  • Event-Based Rules: Define breadcrumbs using ColdBox event names (e.g., main.index, posts.show) or regex patterns.
  • Aliases: Use friendly names for parents like trail.parent("home") instead of main.index.
  • Recursive Hierarchies: Supports dynamic page hierarchies (e.g., Home > Category > Subcategory > Page).
  • Module Friendly: Other modules can ship their own breadcrumb rules, and your app always has the final say.
  • Plug-and-Play: Minimal configuration, easy to override.
  • Mixin Convenience: Easy access in your handlers, layouts, or views simply by calling breadcrumbs()
  • 100% Gluten-Free: No dependencies, no bloat. 💪

Installation

Install Breadcrumb-Buddy via CommandBox:

box install breadcrumb-buddy

This adds the module to your ColdBox app under modules/breadcrumb-buddy, by convention.

Quick Start

  1. Install the Module (see above).
  2. Render Breadcrumbs in your layout or view (e.g., views/layouts/Main.cfm):
// Somewhere in your layout or view
#breadcrumbs().render()#
  1. Customize Rules in config/ColdBox.cfc or /config/modules/breadcrumb-buddy.cfc to override default breadcrumbs. See configuration section below for details.

  2. Styling the Output: By default, the module will output the breadcrumbs in a simple HTML list. You can customize the output by creating your own view and updating the configuration. You can style the breadcrumbs using SASS/CSS classes or frameworks like Bootstrap. Example HTML output after calling render():

<nav aria-label="breadcrumb">
    <ol class="breadcrumb">
        <li class="breadcrumb-item"><a href="./">Home</a></li>
        <li class="breadcrumb-item"><a href="./posts/">Blog</a></li>
        <li class="breadcrumb-item active" aria-current="page">My Blog Post</li>
    </ol>
</nav>

Configuration

Breadcrumb-Buddy is configured in config/ColdBox.cfc or /config/modules/breadcrumb-buddy.cfc. The following settings are available:

  • view: Template for rendering breadcrumbs (default: breadcrumbs/index).
  • viewModule: Module hosting the view (default: breadcrumb-buddy).
  • defaultEvent: The rule to fall back to when no rule matches the current event (default: main.index). Set it to "" to disable the fallback.
  • throwOnError: When true, an error inside one of your rules is rethrown so you see it right away. When false (the default), the error is logged and the page keeps rendering. Tip: turn this on in development.
  • events: Struct of event names (or regex patterns) to breadcrumb rules (closures).
  • aliases: Map of shorthand names to event names that can be used when making parent() calls. (e.g., home: main.index).

Default Configuration

settings = {
    // Override view if desired
    "view" = "breadcrumbs/index",
    // Override view module if desired
    "viewModule" = "breadcrumb-buddy",
    // if no matching rule found, default to this event
    "defaultEvent": "main.index",
    // log rule errors and keep rendering (true rethrows after logging)
    "throwOnError": false,
    // Event-Based breadcrumb rules
    "events" = {
        "main.index": function( trail, event, rc, prc ) {
            trail.push( "Home", event.buildLink( "" ) );
        }
    },
    // Aliases for trail.parent() calls
    "aliases": {
        "home" = "main.index"
    }
};

Overriding Configuration

Add to config/ColdBox.cfc or /config/modules/breadcrumb-buddy.cfc:

// Coldbox.cfc example configuration
moduleSettings = {
    "breadcrumb-buddy": {
        // Override view if desired
        "view" = "breadcrumbs/index",
        // Override view module if desired
        "viewModule" = "breadcrumb-buddy",
        // if no matching rule found, default to this event
        "defaultEvent": "main.index",
        // log rule errors and keep rendering (true rethrows after logging)
        "throwOnError": false,
        // Event-Based breadcrumb rules
        "events" = {
            "main.index": function( trail, event, rc, prc ) {
                trail.push( "Home", event.buildLink( "" ) );
            },
            // Custom event rules for blog post listing page
            "posts.index": function( trail, event, rc, prc ) {
                trail.parent( "home" )
                    .push( "Blog", event.buildLink( "blog" ) );
            },
            // Custom event rules for blog post show page
            "posts.show": function( trail, event, rc, prc ) {
                trail.parent( "home" )
                    .push( "Blog", event.buildLink( "blog" ) )
                    .push( prc.post.getName(), event.buildLink( "posts/#prc.post.getId()#" ) );
            }
        },
        // Aliases for trail.parent() calls
        "aliases": {
            "home" = "main.index"
        }
    };
};

Usage

Defining Rules

Rules are closures in settings.events, keyed by event names (or regex patterns). Each closure receives:

  • trail: BreadcrumbTrail instance to build crumbs.
  • event: ColdBox event object.
  • rc: Request collection
  • prc: Private collection.

The beauty of using closures like this is that you are only limited by your own creativity. You can use any entity, structure, or logic to build your breadcrumb trail.

Example rule:

"posts.index": function( trail, event, rc, prc ) {
    // Inherit home crumbs
    trail.parent( "home" ) 
        // Push the blog post listing page
        .push( "Blog", event.buildLink( "blog" ) );
},
"posts.show": function( trail, event, rc, prc ) {
    // Inherit Blog crumbs
    trail.parent( "posts.index" ) 
         // Push the current post
         .push( prc.post.getName(), event.buildLink( "posts/#prc.post.getId()#" ) ); 
}

How Rules Are Matched

Most of the time, the key is simply your event name. When breadcrumbs are generated for posts.show, Breadcrumb-Buddy looks for a rule with the key posts.show. That's it.

Want one rule to cover several events? Use a regex pattern as the key:

"events" = {
    // one rule for admin:reports.daily, admin:reports.monthly, etc.
    "admin:reports\..*" = function( trail, event, rc, prc ) {
        trail.parent( "home" )
            .push( "Reports", event.buildLink( "admin/reports" ) );
    }
}

A few simple guarantees:

  1. Exact keys always win over regex patterns. If you have both posts.show and posts\..*, the event posts.show uses the exact rule.
  2. Patterns match the whole event name. The pattern posts\..* matches posts.show, but the key posts will NOT match posts.show. If you want "everything under posts", write posts\..*.
  3. If nothing matches, the defaultEvent rule runs (by default, your main.index rule).

Using Aliases

Aliases let you use friendly names:

trail.parent( "home" ); // Resolves to main.index

Define in settings.aliases:

"aliases": {
    "home": "main.index",
    "blog": "posts.index"
}

Note: Aliases work with trail.parent(), not trail.push(). This is by design: push() always adds exactly one literal crumb, so a crumb whose name happens to match an alias (like "Home" and home) can never surprise you. When you want to build on another event's crumbs, use parent().

Trail Object Methods

Each rule closure receives a trail object with methods to build breadcrumbs:

  • trail.push( name, link ):

    • Adds exactly one literal crumb. Never resolves aliases or events — what you push is what you get.
    • name: Text (e.g., "Home Page").
    • link: Optional URL.
  • trail.parent( eventName ):

    • Prepends crumbs from another event (e.g., parent("posts.index")).
    • You may use aliases here (e.g., parent("home")).
    • Don't worry about accidentally creating a loop (A parents B, B parents A). Breadcrumb-Buddy detects it, logs it, and keeps your page rendering.

Adding Breadcrumbs from Your Own Module

Building your own ColdBox module (a CMS, an admin panel, a blog module)? Your module can ship its own breadcrumb rules — and, if it needs to, its own breadcrumb template and its own fallback. Just add a breadcrumb-buddy key to your module's settings in its ModuleConfig.cfc:

// mymodule/ModuleConfig.cfc
function configure(){
    settings = {
        // ... your module's own settings ...

        // Breadcrumbs this module contributes
        "breadcrumb-buddy" = {
            "events" = {
                // module events are named "moduleName:handler.action"
                "mymodule:dashboard.index" = function( trail, event, rc, prc ) {
                    trail.parent( "home" )
                        .push( "Dashboard", event.buildLink( "mymodule/dashboard" ) );
                }
            },
            "aliases" = {
                // ALWAYS prefix an alias with your module name (see below)
                "mymodule:dashboard" = "mymodule:dashboard.index"
            },

            // Optional. These apply ONLY to your module's own events.

            // Render your screens in your own template. `viewModule` defaults to
            // your module name, so you can usually leave it out.
            "view" = "breadcrumbs/index",
            "viewModule" = "mymodule",
            // Where YOUR unmapped screens land. The app's own default is untouched.
            "defaultEvent" = "mymodule:dashboard.index"
        }
    };
}

Breadcrumb-Buddy finds these automatically. No extra wiring needed, and it doesn't matter which module loads first.

The one rule: your module's settings apply to your module only

A module can never change what the app does. view, viewModule and defaultEvent in your contribution are scoped to your own mymodule:* events — an unmapped app event still lands on the app's defaultEvent, and the rest of the site still renders in the app's template.

Settings resolve lowest to highest:

  1. Breadcrumb-Buddy's defaults
  2. the app's settings
  3. your module's contribution (its own events only)
  4. the app's per-module override (below) — the app always gets the last word
  5. an explicit render( view = "...", viewModule = "..." ) argument
Key App config Your module App's modules override
events, aliases yes yes
view, viewModule, defaultEvent yes yes yes
throwOnError app only ignored, logs a warning

Anything else in a contribution is ignored and logs a warning, so a typo tells you instead of quietly doing nothing.

Overriding a module you didn't write

Installed a module whose crumbs don't fit your site? Point it somewhere else from your own config. This is the only way one module's config can affect another, and it wins per key:

moduleSettings = {
    "breadcrumb-buddy": {
        "modules" = {
            // make the admin module's crumbs render in one of OUR views
            // (an empty viewModule means "a view in the app")
            "admin" = { "view" = "breadcrumbs/compact", "viewModule" = "" }
        }
    }
};

Good to know

  • Your app always wins. If the app's config defines the same event key as a module, the app's rule is used, and the conflict is logged so you can spot it.
  • Prefix your aliases with your module name. Aliases are one global namespace shared by the app and every module. A bare dashboard is really a claim on the whole app: it will lose to the app, or beat another module, depending on who got there first. mymodule:dashboard cannot collide with anyone. Breadcrumb-Buddy logs a warning if you forget.
  • Module rules can still use the app's aliases (parent( "home" )), and the app can use aliases contributed by modules.
  • Module event keys already include the module name (mymodule:dashboard.index), so those never clash.

Registering Rules at Runtime

Sometimes config isn't enough — maybe your module builds its rules from a database. You can register rules from code anywhere you can reach the service (handlers, onDIComplete, interceptors):

breadcrumbs().register( "mymodule:pages.view", function( trail, event, rc, prc ) {
    trail.parent( "home" ).push( "Pages", event.buildLink( "mymodule/pages" ) );
} );

breadcrumbs().registerAlias( "pages", "mymodule:pages.view" );

Runtime registrations win over everything else, and registering the same event again simply replaces the old rule.

One-Off Breadcrumbs (No Rules at All)

Need full control for a single page? Skip the rules and pass the crumbs straight to render():

#breadcrumbs().render( breadcrumbs = [
    { "name": "Home", "link": event.buildLink( "" ) },
    { "name": "Something Special", "link": "" }
] )#

Rule Examples

Blog Example

For a blog at /posts/123 (events: posts.index, posts.show ):

"posts.show": function( trail, event, rc, prc ) {
    // Inherit the home crumbs using an alias
    trail.parent( "home" )
        // Push the blog post listing page
        .push( "Blog", event.buildLink( "posts" ) )
        // Push the current post
        .push( prc.post.getName(), event.buildLink( "posts.#prc.post.getId()#" ) );
}

Crumbs:

[
    { "name": "Home", "link": "/" },
    { "name": "Blog", "link": "/posts" },
    { "name": "My Post", "link": "/posts/123" }
]

Page Hierarchy Example

For /about-us/jobs (event: pages.show):

In the following example, we are using a page entity to build the breadcrumb trail. The page entity represents a hierarchical (parent/child) relationship that allows us to traverse the page hierarchy from the current page to the root page. The page object has a method getParent() that returns the parent page object, and a method hasParent() that checks if the page has a parent. The isLoaded() method checks if the page object is loaded. Substitute the page object with your own entity or structure as needed.

// Pages
"pages.show" = function( trail, event, rc, prc ) {
    // prepend the parent page crumbs
    trail.parent( "home" );

    // create a variable to hold the page hierarchy (current to root)
    var pageHierarchy = [ page ]; // add the current page to the hierarchy
    var currentPage = prc.page; // current page object
    var rootUrl = event.buildLink( "" );
    var hasParents = currentPage.hasParent(); // check if the page has a parent

    // Collect pages from current to root
    while( hasParents ) {
        // get the parent page object
        var parent = currentPage.getParent(); 
        
        if( parent.isLoaded() ) {
            // add the parent page to the hierarchy
            pageHierarchy.append( parent ); 
            // set the current page to the parent page
            currentPage = parent; 
            // check if the parent page has a parent
            hasParents = parent.hasParent(); 
        } else {
            // stop if the parent page is not loaded
            hasParents = false; 
        }

    }

    // Build breadcrumb trail in correct order (root to current)
    var urlBuilder = rootUrl;
    // reverse the page hierarchy to get the correct order
    pageHierarchy.reverse().each( function( page ) {
        urlBuilder &= page.slug & "/";
        trail.push( page.name, urlBuilder );
    } );
},

Crumbs:

[
    { "name": "Home", "link": "/" },
    { "name": "About Us", "link": "/about-us" },
    { "name": "Jobs", "link": "/about-us/jobs" }
]

How to Handle 404 Pages and Other Errors

You can define a rule for handling 404 pages or other errors by matching the event name you use for your error handling. For example, if you have a custom error event like errors.onMissingPage, you can define a rule for it in your configuration.

Coldbox Gotcha: If you use an around handler pattern to catch errors, you may need to set the event object in the rc collection to ensure the breadcrumbs are built correctly. This is because calling runEvent() does not change the current event for the request.

There are several ways to work around this behavior. One way is to use the event.overrideEvent() method to set the event in the current request. Side note: overrideEvent() will bypass the event cache for the current event, which may or may not be desirable based on your use case. The second option is to simply set the event in the rc collection. This will not bypass the event cache, and will still trigger the desired breadcrumb rule.

// Common aroundHandler pattern used in base handlers
function aroundHandler( event, targetAction, eventArguments, rc, prc ) {
    
    try{

        // prepare arguments for action call
        var args = {
            event = arguments.event,
            rc    = arguments.rc,
            prc   = arguments.prc
        };

        structAppend( args, eventArguments );

        // execute the action now
        return arguments.targetAction( argumentCollection=args );

    // Catch 404 errors!
    } catch ( NotFound e ) {

        // option 1: Override the event (will bypass the event cache)
        event.overrideEvent( "errors.onMissingPage" ); // bypasses event cache on the current event
        
        // option 2: Set the event in the rc (will not bypass the event cache)
        rc.event = "errors.onMissingPage"; 
        
        return runEvent( 
            event = "errors.onMissingPage",
            eventArguments = {
                "exception": e
            }
        );

    }

}

For a missing page:

"errors.onMissingPage": function( trail, event, rc, prc ) {
    trail.push( "Home", event.buildLink( "" ) )
         .push( "Page Not Found", "" );
}

Crumbs:

[
    { "name": "Home", "link": "/" },
    { "name": "Page Not Found", "link": "" }
]

More Examples

Check out the sample application in the test-harness folder.

Roadmap

  • Optional URL-segment fallback crumbs for pages without rules.

Won't do: trail.push() accepting aliases. Since alias lookups are case-insensitive and aliases usually mirror crumb names (home / "Home"), an alias-aware push() could silently expand a literal crumb into a whole trail. push() stays literal; use trail.parent() (which accepts aliases) to build on another event's crumbs.

Contributing

Do you have any ideas for improving this module? Feel free to submit an issue or, even better, a pull request! Don't forget to add tests for your changes.

About the Author

This module was created by Angry Sam Productions, a California-based web development company. We're passionate about giving back to the dev community through open source because we believe sharing knowledge builds a stronger, better-connected world. If you're interested in contracting us for your next project or learning more, feel free to reach out.

Running Tests

From the root of the project in CommandBox:

# install the test harness dependencies (first time only)
box run-script install:dependencies

# start a test server (or start:lucee6, start:2023, start:boxlang)
box run-script start:lucee5

# run the full test suite from the command line
box testbox run

You can also open http://localhost:60320/tests/runner.cfm in your browser to run the suite there.

About

A Coldbox module for event-based breadcrumb navigation

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages