BMLT Crumb Widget

Crumb Widget

An embeddable NA meeting finder widget for any website.

Live Demo GitHub QA Tests

Overview

Crumb Widget is a self-contained JavaScript widget that queries a BMLT server and renders a fully featured meeting finder — search, filters, list view, and an interactive map.

It ships as a single JavaScript file with all CSS injected at runtime. No stylesheets, no build steps, no framework required on the host page.

Quick Start

To get started, add a container element to your page, point it at your BMLT server, and load the widget script. Here is a complete example:

HTML
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Meeting Finder</title>
  </head>
  <body>
    <div
      id="crumb-widget"
      data-server="https://myserver.com/main_server/"
      data-service-body="3"
    ></div>
    <script type="module" src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>
  </body>
</html>

The widget initializes automatically when the script loads, so no additional JavaScript is needed.

Important: Be sure your page includes <meta name="viewport" content="width=device-width, initial-scale=1.0" /> in the <head>. This is important for proper rendering on mobile devices and small screens.

Example: WordPress

The easiest way to add Crumb Widget to a WordPress site is the official Crumb plugin (GitHub). It wraps this widget in a shortcode with a settings screen — no code required.

Installation

  1. Search for Crumb in the WordPress plugin directory, or upload the plugin ZIP manually.
  2. Activate the plugin.
  3. Go to Settings → Crumb and enter your BMLT server URL.
  4. Add the shortcode to any page or post.

Shortcode

WordPress shortcode
[crumb]

The BMLT server URL set in the plugin settings is used by default. You can override it — or filter by service body — directly in the shortcode:

WordPress shortcode — with overrides
[crumb server="https://your-server/main_server" service_body="42"]

Advanced configuration

All global config options (language, geolocation, columns, map tiles, etc.) can be set from your theme's functions.php via the crumb_config filter:

PHP — functions.php
add_filter( 'crumb_config', function( $config ) {
    return array_merge( $config, [
        'language'          => 'en',
        'geolocation'       => true,
        'geolocationRadius' => 20,
        'height'            => 800,
        'columns'           => [ 'time', 'name', 'location', 'address', 'service_body' ],
    ] );
} );

Switching from Crouton

The Crumb plugin is an alternative to the crouton plugin and can drop in without page edits. Install Crumb, then deactivate crouton — existing pages keep working.

Example: Drupal

Add Crumb Widget to a Drupal 10.3+ or 11 site with the Crumb module. It wraps this widget in a placeable block, a [crumb] shortcode filter for body fields, and a render service for programmatic embedding. Releases are published at github.com/bmlt-enabled/crumb-drupal/releases.

Installation

  1. Download the latest crumb.zip from the releases page.
  2. Extract into web/modules/contrib/ (or web/modules/custom/).
  3. Enable: drush en crumb, or via Extend in the admin UI.
  4. Visit Configuration → Web services → Crumb and enter your BMLT server URL.

Embed via Block

The simplest path: place the Crumb meeting finder block (category: BMLT) in any region via Structure → Block layout. Per-block settings allow overriding server, service body, view, and geolocation.

Embed via shortcode in body fields

Enable the Crumb meeting finder shortcode filter on a text format (Configuration → Content authoring → Text formats and editors), then use:

Drupal shortcode
[crumb]

The BMLT server URL set in the module settings is used by default. You can override it — or filter by service body — directly in the shortcode:

Drupal shortcode — with overrides
[crumb server="https://your-server/main_server" service_body="42" view="map" geolocation="true"]

Advanced configuration

All global config options (language, geolocation, columns, map tiles, etc.) can be set programmatically via hook_crumb_config_alter() in any custom module:

PHP — MYMODULE.module
function MYMODULE_crumb_config_alter(array &$config): void {
  $config = array_merge($config, [
    'language'          => 'en',
    'geolocation'       => TRUE,
    'geolocationRadius' => 20,
    'height'            => 800,
    'columns'           => [ 'time', 'name', 'location', 'address', 'service_body' ],
  ]);
}

Example: Joomla

Add Crumb Widget to a Joomla 4, 5, or 6 site with the Crumb extension. The package bundles a content plugin ({crumb} shortcode in articles) and a site module (for module positions), sharing one configuration. Releases are published at github.com/bmlt-enabled/crumb-joomla/releases.

Installation

  1. Download the latest pkg_crumb.zip from the releases page.
  2. In Joomla admin, go to System → Install → Extensions and upload the package zip.
  3. Enable the plugin: System → Plugins, search "Crumb", open Content - Crumb, set the BMLT server URL, save, and toggle status to Enabled. (Joomla ships content plugins disabled by default.)

Embed via shortcode in articles

With the content plugin enabled, drop the shortcode into any article body:

Joomla shortcode
{crumb}

The BMLT server URL set in the plugin options is used by default. You can override it — or filter by service body — directly in the shortcode:

Joomla shortcode — with overrides
{crumb server="https://your-server/main_server" service_body="42" view="map" geolocation="true"}

Embed via Module

Publish the Crumb module to any module position via Content → Site Modules → New → Crumb. Per-module settings cover server URL, service body filter, default view, and CSS template.

Advanced configuration

For options that aren't surfaced as form fields (language, dark mode, custom map tiles, etc.), use the Widget Configuration (JSON) field on the plugin or module options panel. The JSON is parsed and emitted as window.CrumbWidgetConfig alongside the widget.

Widget Configuration (JSON)
{
  "language": "en",
  "geolocation": true,
  "geolocationRadius": 20,
  "height": 800,
  "darkMode": "auto",
  "columns": ["time", "name", "location", "address", "service_body"]
}

Website Builders

Crumb Widget can also be embedded on common website builders, but the setup varies by platform.

Finding your Service Body ID

Every embed below filters meetings to a specific service body (area, region, or zone) by passing its numeric ID in data-service-body.

The easy way — look up by location. Visit sdle.org, click anywhere on the map (or search for an address), and the popup shows the service body covering that area. Expand Server details in the popup to see both the Server URL and Service Body ID — for either your home server or the aggregator. Click any value to copy it, then paste into data-server and data-service-body.
Don't know your server URL or service body ID? Browse tally.bmlt.app — it lists known BMLT servers and the service bodies on each. Find your area, region, or zone to get both the data-server URL and the data-service-body ID in one place.
Tip: Child service bodies are always included automatically. If you pass a region's ID, meetings from every area under it are included — you do not need to list each child area.

Google Sites

Google Sites supports embedding HTML, CSS, and JavaScript code using Insert → Embed → Embed code. You can also use a full-page embed if you want the meeting finder to live on its own page.

  1. Open your site in Google Sites.
  2. Click InsertEmbed.
  3. Choose Embed code.
  4. Paste your widget markup and script.
  5. Click Insert, then publish your site.
Google Sites embed
<div
  id="crumb-widget"
  data-server="https://myserver.com/main_server/"
  data-service-body="3"
></div>
<script type="module" src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Note: In Google Sites, JavaScript must be included inside <script> tags when using the embed code option. Because Google Sites runs the widget inside a proxied iframe, direct links to specific meetings (deep-linking) are not supported — but all in-widget navigation works normally.

See a live demo on Google Sites →

Squarespace

Squarespace embeds use a Code Block, which accepts raw HTML and JavaScript. Per Squarespace's documentation, Code Blocks with JavaScript enabled are available on the Core, Plus, Advanced, Business, Commerce Basic, and Commerce Advanced plans. The free Personal plan does not support JavaScript in Code Blocks.

Before you start, you need two values:

  1. Log in to Squarespace and open the page where you want the meeting finder.
  2. Click Edit, then hover where you want the widget to go and click the + (Add Block) button.
  3. Search for and insert a Code block.
  4. In the Code block editor, make sure the Mode dropdown is set to HTML and that Display Source is off (otherwise Squarespace will show the raw tags instead of running the code).
  5. Paste the snippet below, replacing data-server and data-service-body with your values.
  6. Click outside the block to close it, then click Save. Publish the page if it is not already live.
Squarespace Code block
<div
  id="crumb-widget"
  data-server="https://myserver.com/main_server/"
  data-service-body="3"
></div>
<script type="module" src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Note: Squarespace renders Code blocks inside the normal page flow, so the widget will take up as much vertical space as it needs. If the preview looks empty, re-open the block and confirm that Display Source is off and the Mode is set to HTML. If you are on a plan that does not allow JavaScript in Code blocks, you will need to upgrade before the widget will load.

Wix

Wix splits the embed across two places: the widget's HTML container lives inside your page as an Embed HTML element (Wix calls this "Custom Embeds → Embed HTML" or "Embed a Widget"), and the loader <script> goes in Settings → Custom Code so it only loads once per visit. Per Wix's documentation, Custom Code requires your site to be published on a paid plan with a connected domain.

Before you start, you need two values:

Step 1 — Add the loader script (once per site):

  1. In your Wix dashboard, go to SettingsAdvancedCustom Code.
  2. Click + Add Custom Code.
  3. Paste the loader snippet below into the code box.
  4. Under Add Code to Pages, choose either All pages or just the page that will show the widget (loading it only where needed is faster).
  5. Set Place Code in to Body – end.
  6. Name it something like Crumb Widget loader and click Apply.
Wix Custom Code — loader
<script type="module" src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Step 2 — Add the widget container on the page:

  1. Open the page in the Wix Editor.
  2. Click + Add ElementsEmbed CodeEmbed HTML (in Wix Studio this is Embed & SocialHTML iframe/Embed).
  3. Drop the element where you want the widget to appear and click Enter Code.
  4. Paste the container snippet below, replacing data-server and data-service-body with your values.
  5. Click Update. Resize the element to roughly the height you want — Wix's Embed HTML has a fixed size, so give it enough room (e.g. 800px tall) for the meeting list and map.
  6. Click Publish.
Wix Embed HTML — container
<div
  id="crumb-widget"
  data-server="https://myserver.com/main_server/"
  data-service-body="3"
></div>
Heads up: Wix's Embed HTML element renders inside an iframe, which means the widget cannot resize the surrounding page and deep-links to individual meetings will not update the browser URL. All in-widget navigation (list, map, meeting detail, filters) still works normally. If you do not see the Custom Code option in Settings, your plan likely does not include it — check that your site is published on a paid plan with a connected domain.

Data Attributes

All configuration is set as data-* attributes on the #crumb-widget div.

Attribute Description
data-server Required Full URL to your BMLT server.
Example: https://example.org/main_server
data-service-body Optional Filter meetings to one or more service bodies. Accepts a single numeric ID or a comma-separated list. Omit to show all meetings on the server.
Single: "42"
Multiple: "42,57,103"
Note: recursive lookup is always enabled — child service bodies are automatically included and cannot be disabled.
data-format-ids Optional Lock the widget to meetings matching one or more format IDs (numeric, server-specific). The filter is applied at the BMLT API level, so only matching meetings are fetched. Useful when you want to embed a list of, e.g., only Open meetings.
Single: "4"
Multiple: "4,7,12" (AND — meetings must match all listed IDs)
Format IDs vary per server. Look them up in BMLT admin or via GetFormats. For server-agnostic filtering by code (e.g. O, BT), use the formats global config option instead.
data-view Optional Default view when the widget loads.
Values: list (default), map, or both
both shows a fixed-height map above the full meeting list with no list/map toggle.
Can be overridden at runtime by the ?view= query parameter (see URL Query Parameters).
data-columns Optional Comma-separated list of columns to show in list view. Same allowed values as the columns global config option: time, distance, name, location, address, service_body. Overridden by CrumbWidgetConfig.columns when both are set.
Example: data-columns="time,name,address"
data-path Optional Enables History API routing (clean URLs without #) and sets the base path. Without this attribute, hash-based routing is used (default).
Example: data-path="/meetings" produces URLs like /meetings/monday-night-meeting-42
Use data-path="" for path routing with no prefix (e.g. in iframe embeds).
See Routing for details.
data-query Optional Raw BMLT query string passed through to the server verbatim, for filters the structured options can't express (e.g. meeting_key_value[] matching multiple values). Replaces the default service-body load entirely — data-service-body, data-format-ids, and ?services are ignored when this is set, so the embedder controls the query end-to-end. Geolocation is forced off because lat_val/long_val/geo_width can't safely be layered on top of an arbitrary query. page_size, get_used_formats=1, and lang_enum are appended automatically if you don't include them, so meetings + formats still arrive in a single round-trip.
Example: data-query="meeting_key=location_nation&meeting_key_value[]=USA&meeting_key_value[]=US"
data-update-url Optional Show an "Update Meeting Info" link at the bottom of every meeting detail panel, pointing at a URL template you control. The template is expanded per meeting using the tokens {meeting_id}, {meeting_name}, {server_url}, and {return_url} — each token's value is URL-encoded on substitution. Designed for integration with bmlt-workflow's update form, any custom hosted form, or a mailto: link. Unknown tokens are left intact. The link opens in a new tab. The button is hidden when the attribute is unset. See the Update Meeting Info Link section below for examples.
bmlt-workflow: data-update-url="https://example.org/meeting-update/?meeting_id={meeting_id}"

Global Config Object

For options not available as data attributes, define CrumbWidgetConfig as a global variable before loading the script.

HTML
<script>
  var CrumbWidgetConfig = {
    view: 'map'
  };
</script>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>
Property Type Description
view 'list' | 'map' | 'both' Override the default view. Takes precedence over data-view. 'both' shows a fixed-height map above the full meeting list; the list/map toggle is hidden.
language string Override the UI language (e.g. 'es', 'fr'). Defaults to navigator.language, falling back to English. Supported: en, es, fr, de, pt, it, sv, da.
columns string[] Columns to show in list view. Default: ['time', 'distance', 'name', 'location', 'address']. Takes precedence over data-columns. Omit any column name to hide it. Available values: time, distance (only renders when geolocation is active), name, location (venue/building name), address (street address with in-person/online badges), service_body. service_body is hidden by default — add it explicitly to show the service body name.
geolocation boolean Enable location-based search: a Near Me button (auto-geolocates the user via the browser, requires HTTPS) and a Search location mode on the search input that resolves a typed city, postal code, or address (e.g. Edgartown, MA or 02539) via OpenStreetMap Nominatim. Default: false for most servers, true when data-server points at aggregator.bmltenabled.org with no service-body set (the unconstrained aggregator needs a location to return useful results). If browser geolocation fails or is denied, an error is shown — no fallback load occurs. Note: serviceBodyIds filters have no effect on location searches; results are determined entirely by proximity.
geolocationRadius number Controls the search radius when geolocation is used. A positive value sets a fixed radius in miles (or km, per distanceUnit) and caps the Near Me distance dropdown. A negative integer uses BMLT auto-radius mode — the server expands the search until it finds roughly that many meetings (e.g. -50 finds ~50 nearby meetings); the distance dropdown then shows all distanceOptions without a cap. Default: -50.
distanceOptions number[] Distance values (in distanceUnit) shown in the Near Me dropdown. When geolocationRadius is a positive number, options above it are hidden and it is appended as the last option if not already present. When geolocationRadius is negative (auto-radius), all options are shown as-is. Default: [5, 10, 15, 25, 50, 100].
distanceUnit 'mi' | 'km' Unit label shown next to distances in the Near Me dropdown. Default: auto-detected from the resolved locale (en-US, en-GB, en-LR, my-MM'mi'; everything else → 'km'). Set explicitly to force a unit.
nowOffset number Minutes of grace period before a meeting is considered past. Meetings that started within the last nowOffset minutes still appear at the top of the list; older meetings on today's weekday roll over to the end of the list (as if they occur next week). Default: 10. Set to 0 to roll over immediately once a meeting's start time has passed.
hideHeader boolean Hide the "Meeting Finder" title bar and meeting count at the top of the widget. Default: false.
darkMode 'auto' | true | false Built-in dark color scheme. 'auto' follows the visitor's OS preference (prefers-color-scheme); true forces dark mode regardless of OS setting; false (default) disables it. Works alongside dark mode tiles.
height number Widget height in pixels. Equivalent to the data-height attribute. Omit to let the widget grow to its natural content height.
formats string[] Lock the widget to meetings matching one or more format key strings (e.g. 'O', 'BT', 'WC'). Filtering is applied after the API response, so all meetings are still fetched and the format-key filter narrows the result set client-side. Multiple keys are AND-combined — a meeting must match every listed key. Key strings are stable across BMLT servers, so this is the best option when you want server-agnostic filtering. For server-side filtering by numeric format ID, use data-format-ids instead. Default: [] (no lock).
Example: formats: ['O']
basePath string Base path for History API routing (e.g. '/meetings'). Equivalent to the data-path attribute — produces clean URLs without the # fragment. See Routing for details.
query string Raw BMLT query string passed through to the server verbatim. Equivalent to the data-query attribute (the attribute takes precedence). Use this for filters the structured options can't express — e.g. meeting_key_value[] matching multiple values, or combinations of StartsAfterH/EndsBeforeH. Replaces the default load entirely (serviceBodyIds and the numeric format lock are ignored), and forces geolocation to false — Near Me, typed-location search, and "Search this area" are all disabled because geo params can't safely be layered on top of an arbitrary query. page_size, get_used_formats=1, and lang_enum are appended automatically if you don't include them. Client-side formats key filtering (see above) still applies after the fetch.
Example: query: 'meeting_key=location_nation&meeting_key_value[]=USA'
updateUrl string URL template for the "Update Meeting Info" link at the bottom of the meeting detail panel. Equivalent to the data-update-url attribute (the attribute takes precedence). Tokens: {meeting_id}, {meeting_name}, {server_url}, {return_url} — URL-encoded on substitution. Works with bmlt-workflow's update form, any custom hosted form, or mailto: URLs. The link opens in a new tab. Hidden when unset. See the Update Meeting Info Link section below for examples.
map.tiles TilesConfig Custom map tile provider. Replaces the default OpenStreetMap tiles. See Tile Provider below.
map.tiles_dark TilesConfig Alternate tile provider used when prefers-color-scheme: dark. Swaps automatically on OS theme change. See Dark Mode Tiles below.
map.markers.location MarkerConfig Custom map marker for meeting locations. Replaces the default NA marker. See Marker Config below.
HTML — all options (kitchen sink)
<script>
  var CrumbWidgetConfig = {
    view: 'list',       // 'list' | 'map' | 'both'
    language: 'en',            // override UI language; defaults to navigator.language
    columns: ['time', 'distance', 'name', 'location', 'address', 'service_body'],
    geolocation: false,        // Near Me + typed location search (defaults true on the unconstrained aggregator)
    geolocationRadius: -50,    // negative = BMLT auto-radius (~50 meetings); positive = fixed miles/km
    nowOffset: 10,             // minutes before a meeting is considered past and rolls to end
    hideHeader: false,         // hide the "Meeting Finder" title bar
    darkMode: 'auto',          // 'auto' | true | false
    height: 600,               // widget height in pixels
    formats: ['O'],            // lock to meetings with these format key strings (client-side, AND)
    query: 'meeting_key=location_nation&meeting_key_value[]=USA', // raw BMLT query; replaces default load, disables geolocation
    basePath: '/meetings',     // History API base path; equivalent to data-path
    updateUrl: 'https://example.org/meeting-update/?meeting_id={meeting_id}', // "Update Meeting Info" link template
    map: {
      tiles: {
        url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
      },
      tiles_dark: {
        url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png',
        attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a>'
      },
      markers: {
        location: {
          html: '<img src="https://example.com/my-marker.png">',
          width: 23,
          height: 33
        }
      }
    }
  };
</script>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Language

The widget automatically detects the visitor's language from navigator.language and falls back to English. Override it with the language config property:

HTML
<script>
  var CrumbWidgetConfig = {
    language: 'es'
  };
</script>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Language codes are matched on the base tag, so 'fr-CA' uses fr. Supported languages:

Code Language
en English
es Spanish
fr French
de German
pt Portuguese
it Italian
sv Swedish
da Danish
el Greek
fa Persian
pl Polish
ru Russian
ja Japanese

Geolocation

Defaults to true when data-server points at the BMLT aggregator (aggregator.bmltenabled.org) with no service-body set, since the aggregator otherwise has no way to scope a search; defaults to false for every other configuration. Set geolocation explicitly in CrumbWidgetConfig to override either default.

Enable the Near Me button with geolocation: true. When enabled, the widget also attempts to geolocate the user automatically on page load and show meetings nearby. Requires a secure context (HTTPS).

If geolocation is denied or unavailable, an error message is displayed — there is no silent fallback to loading all meetings. This is intentional: on large servers without service body scoping, loading all meetings could return tens of thousands of results.

Service body filters have no effect on geolocation searches. When using Near Me, results are determined entirely by the user's coordinates and geolocationRadius. If no serviceBodyIds are configured, the Near Me button cannot be toggled off — geolocation is the only data source.

The Near Me button opens a distance dropdown so users can choose how far to search. Selecting a radius applies a client-side distance filter on the already-loaded meetings — no new network request is made when changing radius. When geolocationRadius is a positive number, the available distances come from distanceOptions capped at that value; options with no meetings in range are hidden automatically. When geolocationRadius is a negative integer (auto-radius), all distanceOptions are shown without a cap — the initial search finds roughly that many meetings, and picking a distance from the dropdown switches to a fixed radius. Use distanceUnit to display kilometres instead of miles.

While in map view with Near Me active, panning or zooming the map reveals a Search this area button at the top of the map. Clicking it reloads meetings centered on the current map viewport, using the same selected radius. The map stays at the user's position — it does not jump to fit the new results.

The search input also gains a small mode dropdown: users can switch between Filter meetings (the default — a client-side text filter over already-loaded meetings) and Search location. In location mode, typing a city, postal code, or address (e.g. Edgartown, MA or 02539) and pressing Enter resolves the place via OpenStreetMap Nominatim and reloads meetings near the resulting coordinates. The radius used is the last Near Me choice if any, otherwise geolocationRadius. Geocoding traffic goes to nominatim.openstreetmap.org directly — no API key is required, but consumers should respect Nominatim's usage policy (max one request per second, requests must include a User-Agent identifying your application — bmlt-query-client sets one by default).

HTML
<script>
  var CrumbWidgetConfig = {
    geolocation: true,
    geolocationRadius: -50,        // auto-radius: find ~50 nearby meetings (default); use positive number for fixed miles/km
    distanceOptions: [5, 10, 25, 50], // defaults to [5,10,15,25,50,100]
    distanceUnit: 'mi'             // 'mi' or 'km'; omit to auto-detect from locale
  };
</script>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Update Meeting Info Link

Add an "Update Meeting Info" button at the bottom of every meeting detail panel by setting data-update-url (or updateUrl in the global config) to a URL template. The widget substitutes per-meeting tokens at click time and opens the result in a new tab. The button is hidden when the option is unset, so you can opt in selectively.

Supported tokens (each value is URL-encoded on substitution; unknown {tokens} are left intact):

Token Value
{meeting_id} BMLT meeting id_bigint — the canonical identifier most update forms expect.
{meeting_name} Human-readable meeting name. Useful in mailto: subjects.
{server_url} The configured BMLT root server URL (the data-server value).
{return_url} The current page URL — useful as a back-link or audit reference in submissions.

Example: bmlt-workflow integration

bmlt-workflow is a WordPress plugin that exposes a meeting update form via the [bmltwf-meeting-update-form] shortcode. The form's JavaScript reads ?meeting_id= from the query string to pre-select the meeting, so the template only needs the {meeting_id} token:

HTML
<div
  id="crumb-widget"
  data-server="https://bmlt.example.org/main_server/"
  data-service-body="3"
  data-update-url="https://your-wordpress-site.example.org/meeting-update-form/?meeting_id={meeting_id}"
></div>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Example: mailto link

Don't have a hosted form? Point at a mailto: URL — the user's mail client opens with the subject and body pre-filled. Tokens are URL-encoded, so they're safe inside mailto: query parameters:

HTML
<div
  id="crumb-widget"
  data-server="https://bmlt.example.org/main_server/"
  data-service-body="3"
  data-update-url="mailto:webservant@example.org?subject=Update%20{meeting_name}&body=Meeting%20ID%3A%20{meeting_id}%0AReference%3A%20{return_url}"
></div>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Example: any custom endpoint

Any URL that accepts a meeting identifier in the query string works. The link opens in a new tab, so your endpoint can be a full page (a Google Form, an Airtable form, a custom CMS, etc.). Pass {return_url} to give your form a back-link to the meeting page:

HTML
<div
  id="crumb-widget"
  data-server="https://bmlt.example.org/main_server/"
  data-service-body="3"
  data-update-url="https://forms.example.org/report?id={meeting_id}&name={meeting_name}&back={return_url}"
></div>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

URL Query Parameters

The ?view= query parameter sets the initial view and overrides data-view and view:

Value Behaviour
list Force list view on load
map Force map view on load
both Show map above the full meeting list; list/map toggle is hidden
auto Geolocate on load — success shows map with nearby results; failure falls back to list with all meetings

Example: https://example.org/meetings/?view=auto

When geolocation: true is set, the widget auto-prompts on load — including for ?view=map and ?view=both. ?view=list is the explicit opt-out: it suppresses the auto-prompt so the user can still trigger it manually via the Near Me button.

Three additional query parameters override their corresponding embed configuration so a single page can be re-targeted via URL: ?services= overrides data-service-body (comma-separated service body IDs); ?format_ids= overrides data-format-ids (comma-separated numeric format IDs, server-side filter); and ?formats= overrides CrumbWidgetConfig.formats (comma-separated format key strings, client-side filter). All three accept an empty value to clear the configured filter (e.g. ?formats=).

CSS Variables

The widget exposes CSS custom properties so you can theme it without touching JavaScript. Set any of these on the #crumb-widget element — only specify the ones you want to override.

Variable Default Controls
--bmlt-font-family system-ui, -apple-system, sans-serif Widget font stack
--bmlt-font-size 16px Base font size
--bmlt-background #ffffff Widget and controls bar background
--bmlt-text #111827 Primary text color
--bmlt-border #e5e7eb Borders and dividers
--bmlt-accent #2563eb Active buttons, links, focus indicators
--bmlt-accent-light #eff6ff Row hover, active filter panel background
--bmlt-border-radius 8px Card and button corner radius
--bmlt-row-alt #f9fafb Alternating (even) row background
--bmlt-in-person #15803d In-person badge text
--bmlt-in-person-bg #dcfce7 In-person badge background
--bmlt-virtual #1d4ed8 Virtual badge text
--bmlt-virtual-bg #dbeafe Virtual badge background
--bmlt-in-progress-bg #e0e7ff In-progress banner and row background
--bmlt-in-progress-text #3730a3 In-progress banner text
--bmlt-surface #ffffff Input, button, and dropdown panel backgrounds
--bmlt-hover #f9fafb Neutral hover state (buttons and rows not using accent)
--bmlt-divider #f3f4f6 Internal row and list divider lines
--bmlt-text-secondary #6b7280 Secondary / muted text (table headers, time, location)
--bmlt-text-muted #9ca3af Very muted text (footer count, empty-state messages)
CSS — change accent color and remove rounded corners
#crumb-widget {
  --bmlt-accent: #dc2626;
  --bmlt-accent-light: #fef2f2;
  --bmlt-border-radius: 0px;
}

Dark Mode

The widget ships with a built-in dark palette. Enable it with the darkMode config option — no extra CSS required. Works alongside the dark mode tile option.

HTML — follow OS preference
<script>
  var CrumbWidgetConfig = {
    darkMode: 'auto'
  };
</script>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>
HTML — always dark
<script>
  var CrumbWidgetConfig = {
    darkMode: true
  };
</script>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

To use a custom dark palette instead, override the CSS variables directly:

CSS — custom dark theme via media query
@media (prefers-color-scheme: dark) {
  #crumb-widget {
    --bmlt-background:     #1e293b;
    --bmlt-text:           #f1f5f9;
    --bmlt-border:         #334155;
    --bmlt-accent:         #60a5fa;
    --bmlt-accent-light:   #1a3460;
    --bmlt-row-alt:        #243044;
    --bmlt-in-person:      #86efac;
    --bmlt-in-person-bg:   #14532d;
    --bmlt-virtual:        #93c5fd;
    --bmlt-virtual-bg:     #1e3a5f;
    --bmlt-surface:        #2a3a50;
    --bmlt-hover:          #2d4060;
    --bmlt-divider:        #263244;
    --bmlt-text-secondary: #94a3b8;
    --bmlt-text-muted:     #64748b;
  }
}

CSS Helper Classes

These classes are applied internally and can also be targeted in your own CSS for further customization.

Class Used on
bmlt-btn-primary Filled accent buttons — active filter chips, view toggle, Join Meeting
bmlt-btn-secondary Outlined buttons — Get Directions
bmlt-link Link-style buttons — Back, email contact
bmlt-badge-in-person In-person venue badge
bmlt-badge-virtual Virtual venue badge
bmlt-card Detail section cards
bmlt-row Meeting list table rows
CSS — example: override the outlined button hover
#crumb-widget .bmlt-btn-secondary:hover {
  background-color: transparent;
  opacity: 0.8;
}

Tile Provider

By default the map uses OpenStreetMap tiles. Switch to any Leaflet-compatible tile provider by setting map.tiles.

Property Type Description
url string Tile URL template. Use {z}, {x}, {y} placeholders (and {r} for retina where supported).
attribution string Attribution HTML displayed in the map's attribution control. Required by most tile providers' terms of service.
HTML — Stadia Maps example
<script>
  var CrumbWidgetConfig = {
    map: {
      tiles: {
        url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png',
        attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
      }
    }
  };
</script>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Dark Mode Tiles

Set map.tiles_dark to use a different tile layer for visitors with prefers-color-scheme: dark. The layer swaps automatically when the OS theme changes — no page reload needed. If omitted, the same tiles are used in all color schemes.

HTML — light + dark tiles
<script>
  var CrumbWidgetConfig = {
    map: {
      tiles: {
        url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png',
        attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
      },
      tiles_dark: {
        url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png',
        attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
      },
    }
  };
</script>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>
HTML — Mapbox example
<script>
  var CrumbWidgetConfig = {
    map: {
      tiles: {
        url: 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=<pk.your.access.token>',
        attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a>, Imagery &copy; <a href="https://www.mapbox.com/">Mapbox</a>',
      },
      tiles_dark: {
        url: 'https://api.mapbox.com/styles/v1/mapbox/dark-v10/tiles/{z}/{x}/{y}?access_token=<pk.your.access.token>',
        attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a>, Imagery &copy; <a href="https://www.mapbox.com/">Mapbox</a>',
      },
    }
  };
</script>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Google Maps tiles: Google's tile API requires a session token obtained server-side (or via a short-lived client-side fetch). Because the token must be fetched before CrumbWidgetConfig is set, you need to load https://cdn.aws.bmlt.app/crumb-widget.js dynamically after the token is ready. The example below uses the Google Map Tiles API — you'll need a referer-restricted API key with the Map Tiles API enabled.

JavaScript
<script>
(async () => {
  const key = 'YOUR_REFERER_RESTRICTED_GOOGLE_API_KEY';
  const { session } = await fetch(
    `https://tile.googleapis.com/v1/createSession?key=${encodeURIComponent(key)}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ mapType: 'roadmap', language: 'en-US', region: 'US' })
    }
  ).then(r => r.json());

  window.CrumbWidgetConfig = {
    map: {
      tiles: {
        attribution: '&copy; Google',
        url: `https://tile.googleapis.com/v1/2dtiles/{z}/{x}/{y}?session=${session}&key=${key}`
      }
    }
  };

  const s = document.createElement('script');
  s.src = 'https://cdn.aws.bmlt.app/crumb-widget.js';
  document.body.appendChild(s);
})();
</script>

Marker Config

The map.markers.location object lets you replace the default NA map pin with any image or inline SVG.

Property Type Description
html string HTML rendered inside the marker element. Typically an <img> tag or inline SVG.
width number Icon width in pixels.
height number Icon height in pixels. The anchor point is set to the bottom-center of the icon.
HTML — custom marker example
<script>
  var CrumbWidgetConfig = {
    map: {
      markers: {
        location: {
          html: '<img src="https://example.com/my-marker.png">',
          width: 23,
          height: 33
        }
      }
    }
  };
</script>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Example: Basic Embed

Show all meetings on a public BMLT server:

HTML
<div
  id="crumb-widget"
  data-server="https://latest.aws.bmlt.app/main_server"
></div>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Example: Filter by Service Body

Show only meetings belonging to service body 42 (and its children):

HTML
<div
  id="crumb-widget"
  data-server="https://bmlt.sezf.org/main_server"
  data-service-body="42"
></div>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Example: Multiple Service Bodies

Pass a comma-separated list to include meetings from several service bodies:

HTML
<div
  id="crumb-widget"
  data-server="https://bmlt.sezf.org/main_server"
  data-service-body="42,57,103"
></div>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Example: Default to Map View

HTML
<div
  id="crumb-widget"
  data-server="https://bmlt.sezf.org/main_server"
  data-service-body="42"
  data-view="map"
></div>
<script src="https://cdn.aws.bmlt.app/crumb-widget.js"></script>

Routing

By default, the widget uses hash-based routing for meeting detail URLs (e.g. example.com/#/monday-night-meeting-42). This works out of the box on any hosting with no server configuration.

Pretty URLs (History API)

For clean URLs without the # fragment, add a data-path attribute with your base path:

HTML
<div
  id="crumb-widget"
  data-server="https://myserver.com/main_server/"
  data-service-body="3"
  data-path="/meetings"
></div>

This produces URLs like example.com/meetings/monday-night-meeting-42. Your server must be configured to serve the page for all sub-paths under the base path so that direct visits and page refreshes work:

Server Configuration
WordPress add_rewrite_rule('^meetings/(.*)?', 'index.php?pagename=meetings', 'top');
Drupal Set Base Path for Pretty URLs in Configuration → Web services → Crumb; the module registers the necessary route automatically.
Joomla Set Base Path for Pretty URLs on the Content - Crumb plugin's options (or on the module). Combine with Joomla's Search Engine Friendly URLs + URL Rewriting options under System → Global Configuration → Site.
Nginx location /meetings { try_files $uri /meetings/index.html; }
Apache FallbackResource /meetings/index.html
Cloudflare Pages Add a _redirects file to your publish directory containing /meetings/* /meetings/index.html 200 (or /* /index.html 200 when serving at the root).

Embedding in iframes (Google Sites, etc.)

When embedding in iframe-based hosts like Google Sites, all in-widget navigation (clicking meetings, back button, sibling links) works normally. However, deep-linking to a specific meeting via URL is not supported because the widget runs inside a proxied iframe and cannot control the parent page's URL.

No special configuration is needed — the default hash-based routing works inside iframes. The data-path attribute is not needed for iframe embeds.

Features

Browser Support

All modern browsers. Internet Explorer is not supported.

FAQ & Troubleshooting

Nothing is showing up — the widget area is blank

The two most common causes:

The map is blank or won't load

How do I change the colors to match my site?

Override the widget's CSS custom properties — see CSS Variables. For dark mode, see Dark Mode. You can also use one of the built-in helper themes — see CSS Helper Classes.

My service body filter shows no meetings — why?

Can I embed this inside an iframe (Google Sites, etc.)?

Yes. Set data-router="hash" so individual meeting links use #-style URLs, which work inside iframes where the parent page controls the address bar. See Routing and the Google Sites example.

I'm still stuck — where can I get help?

npm Package

In addition to the CDN script-tag embed, Crumb Widget is available as an npm package for use in JavaScript/TypeScript projects (React, Svelte, Vue, plain Vite, etc.).

Installation

bash
npm install crumb-widget leaflet

leaflet is a peer dependency — install it alongside crumb-widget so consumers can dedupe and pin their preferred version.

Usage

Import mountCrumbWidget and call it with a DOM element and your configuration. The widget mounts itself and injects its own CSS — no separate stylesheet import needed.

TypeScript / ESM
import { mountCrumbWidget } from 'crumb-widget';

mountCrumbWidget(document.getElementById('my-widget'), {
  serverUrl: 'https://bmlt.example.org/main_server/',
  serviceBodyIds: [1, 2, 3],
});

In a Svelte component, use onMount to ensure the element exists before mounting:

Svelte
<script lang="ts">
  import { onMount } from 'svelte';
  import { mountCrumbWidget } from 'crumb-widget';

  let widgetEl: HTMLDivElement;

  onMount(() => {
    mountCrumbWidget(widgetEl, {
      serverUrl: 'https://bmlt.example.org/main_server/',
      serviceBodyIds: [1, 2, 3],
    });
  });
</script>

<div bind:this={widgetEl} id="crumb-widget" style="height:600px"></div>

Options

All options from Global Config are available, plus two additional required fields:

Option Type Required Description
serverUrl string Yes URL of the BMLT server.
serviceBodyIds number[] No Service body IDs to filter by. Omit or pass [] to show all.
view 'list' | 'map' | 'both' No Initial view. Defaults to 'list'. 'both' shows map above list with no toggle.
geolocation boolean No Enable Near Me + typed location search (city, zip, address). Defaults to false, except true when serverURL is the aggregator with no serviceBodyIds.
geolocationRadius number No Search radius. Positive = fixed miles/km (caps the distance dropdown). Negative integer = BMLT auto-radius: find ~N nearby meetings (e.g. -50). Defaults to -50.
distanceOptions number[] No Distance values shown in the Near Me dropdown. Defaults to [5,10,15,25,50,100].
distanceUnit 'mi' | 'km' No Unit label for distances. Auto-detected from the resolved locale when omitted (e.g. en-US'mi', de-DE'km').
darkMode false | true | 'auto' No Dark mode setting. Defaults to false.
language string No BCP-47 language tag (e.g. 'es'). Defaults to browser locale.
columns Column[] No Table columns to display. See Global Config.
nowOffset number No Minutes of grace period before a meeting is considered past. Defaults to 10; set to 0 to roll over immediately.
hideHeader boolean No Hide the "Meeting Finder" title bar and meeting count. Defaults to false.
height number No Fixed widget height in pixels. Omit for natural height.
formats string[] No Format key strings (e.g. ['O', 'BT']) to lock the widget to. Client-side filter after fetch. Use serviceBodyIds-style numeric filtering via the widget's data-format-ids attribute for server-side filtering.
basePath string No Base path for History API routing (e.g. '/meetings'). Produces clean URLs without the # fragment. Equivalent to the data-path attribute.
updateUrl string No URL template for the "Update Meeting Info" link on the meeting detail panel. Supports {meeting_id}, {meeting_name}, {server_url}, {return_url} tokens (URL-encoded). Works with bmlt-workflow, hosted forms, or mailto: URLs.
map object No Tile provider and marker config. See Tile Provider.
Note: The npm package bundles Svelte and all styles into a single JS file. leaflet is a peer dependency (install it alongside crumb-widget) so you can dedupe with any other Leaflet-using code in your app. Each call to mountCrumbWidget is independent, but multiple widgets on the same page are not currently supported.

Philosophy

This project aims to cover the needs of most NA service bodies well — not every possible use case. It is intentionally kept simple and focused. New features are weighed against the cost to the codebase; niche requests that bloat the project for everyone are generally out of scope. If you need deep customization, this may not be the right tool.

Acknowledgements

Crumb Widget owes a great deal to the work of the Code4Recovery community, and in particular to tsml-ui — a React-based meeting finder that inspired many of the UX patterns and solved many of the same real-world problems. Crumb Widget exists precisely because BMLT's data model differs enough from tsml-ui's assumptions that it needed a first-class UX of its own rather than a shoehorned fit. If you're not running a BMLT server, tsml-ui is what you should be using.