Skip to main content

Example : Facet Combo → Category Redirect

Logic

On a source category page, redirect a shopper to a more specific category when their selected filters match a specific combination — for example, picking Color: Blue + Size: Large on a generic shirts collection jumps them to a dedicated Blue Large Shirts collection instead of just narrowing the parent.

The redirect uses the storefront's existing categories_navigation request (the same one Fast Simon would fire if the shopper clicked a link to that category), so SEO, personalization, and analytics all behave like a normal category visit.

The merchant decides the matching policy — Fast Simon only exposes a safe, throttled API (window.SerpOptions.navigateToCategory).

Works on any platform — Shopify, BigCommerce, Magento, WooCommerce, Wix, and custom storefronts. The snippet is plain JavaScript with no platform-specific dependencies. URL examples below use Shopify's /collections/... path convention; substitute whatever path your platform produces. The product type used in the example (shirts) is purely illustrative — the same pattern works for any vertical.

Safety

  • Inert until the snippet is added to your storefront. No behavioral change for merchants who don't use this customization.
  • Loop-safe: navigateToCategory has built-in cooldown (750 ms), in-flight lock, per-page max (5 by default), and popstate cooldown guards. The snippet cannot create a request loop even if the listener is buggy.
  • Instant rollback: delete the <script> block to revert; no Fast Simon redeploy needed.

Platform support

The snippet is plain JavaScript and works on every Fast Simon storefront — Shopify, BigCommerce, Magento, WooCommerce, Wix, custom storefronts. Place it in your site's <head>, after the Fast Simon script tag, using whatever templating mechanism your platform uses:

PlatformWhere to put it
Shopifytheme.liquid <head>, or a snippet file included via {% include 'fastsimon-navigate-to-category' %}
BigCommerceStorefront > Script Manager (Footer or Head; the snippet auto-waits for fast-serp-ready)
MagentoA custom layout XML or theme <head> block
Wix / Custom<head> of the page template, or any global scripts injection point
Hosted/SaaSTag manager (Google Tag Manager, etc.) — load on All Pages

The snippet auto-waits for fast-serp-ready, so load order doesn't matter — paste it once and it activates when Fast Simon loads.

Setup steps (start here)

End-to-end walkthrough. Allow ~30 minutes for the first setup; subsequent combos take 1–2 minutes each.

Step 1 — Plan your redirects

Before touching any code, list out the combinations you want, in plain words:

"On the Shirts collection, when shoppers pick Color: Blue AND Size: Large, send them to the Blue Large Shirts collection instead."

A spreadsheet helps:

Source categoryCombo (filter values)Target category
ShirtsColor: Blue + Size: LargeBlue Large Shirts
ShirtsColor: BlueBlue Shirts
DressesColor: RedRed Dresses

If you have a combo that should send shoppers to different targets depending on which source they came from (e.g., Size: 8x10 from collection-A goes to one place, but Size: 8x10 from collection-B goes to another), note that down — you'll use Pattern B below.

Step 2 — Make sure each target collection exists in your store

The redirect can only land on a collection that already exists. Open your e-commerce admin (Shopify, BigCommerce, etc.) and confirm that every target in your spreadsheet is a real collection with products. If any are missing, create them first.

Step 3 — Get the Fast Simon categoryID for each target

For every target collection in your spreadsheet, you need its Fast Simon category ID (a long number).

  1. Open the Fast Simon dashboard.
  2. Go to Catalog → Collections.
  3. Find each target collection by name.
  4. Copy the ID column value into your spreadsheet.

The category ID format is identical on every platform.

Step 4 — Note the storefront URL of each target

For every target collection, also record the storefront URL path (the part after https://yourstore.com) in your spreadsheet. Examples by platform:

PlatformURL path example
Shopify/collections/blue-large-shirts
BigCommerce/blue-large-shirts/
Magento/blue-large-shirts.html
WooCommerce/product-category/blue-large-shirts/
Customwhatever your storefront produces

Use absolute paths (start with /).

Step 5 — Pick a customization pattern

Based on your spreadsheet:

  • Pattern A — Flat map. Every combo redirects to one target, regardless of which source the shopper came from. Simplest.
  • Pattern B — Per-source map. The same combo needs different targets depending on the source category.
  • Pattern C — Both. A few source-specific rules plus some global ones.

Most merchants start with Pattern A.

Step 6 — Fill the snippet

Copy the JS Code block below and paste it into a text editor. Edit only the CUSTOMIZE block at the top:

  1. Choose the right map shape for your pattern (the Customization patterns section below shows what each shape looks like).
  2. Add one entry per row from your spreadsheet. The map key is the canonical combo string (e.g., "color:blue|size:large" — see Validating canonical keys for how to confirm the exact format your storefront emits).
  3. Set DEBUG = true for now — you'll flip it back to false after testing.

Step 7 — Add the snippet to your storefront

Place it in your site's <head>, after the Fast Simon script tag, using the platform-appropriate method from the Platform support table above.

For Shopify specifically, the cleanest approach is:

  1. In your Shopify admin, go to Online Store → Themes → … → Edit code.
  2. Open theme.liquid.
  3. Find the line that loads the Fast Simon script (usually inside <head>).
  4. Paste the snippet immediately after it.
  5. Save.

Test on an unpublished theme first. Duplicate your live theme, paste the snippet there, preview the duplicate, and only publish after Step 8 confirms it works.

Step 8 — Test with DEBUG mode

  1. Open your storefront in a browser.
  2. Open DevTools → Console (F12).
  3. Visit your source category (e.g., https://yourstore.com/collections/shirts).
  4. Look for [FastSimon redirect] ready. ... — confirms the snippet loaded.
  5. Click each filter you care about, one at a time. After each click, the console should print:
    [FastSimon redirect] canonical key = "color:blue"
  6. If the printed key matches what you put in your map → on the click that completes a matching combo, you'll see [FastSimon redirect] MATCH — navigating to /collections/... and the page will redirect.
  7. If the printed key does NOT match what you put in your map → copy the printed key verbatim into your map and reload. The most common mismatch is the facet name itself (e.g., your storefront emits "colors" plural instead of "color" singular).
  8. Verify the redirect lands on the correct collection with the correct products.
  9. Hit browser back — should return to the source category with both filters still applied. No second redirect should fire.

Step 9 — Go live

Once everything works in testing:

  1. Set DEBUG = false in the snippet (otherwise every shopper's console fills with logs).
  2. Save the theme.
  3. Publish.

Step 10 — Add more combos later

To add a new redirect after going live: open the snippet, add a new entry to the map, save. No Fast Simon redeploy needed. To remove a redirect, delete the entry. To roll back the entire feature, delete the <script> block.

Quick troubleshooting

SymptomLikely cause
Console shows navigateToCategory missing — feature disabledYour Fast Simon serving/SSR build is too old. Contact Fast Simon support to confirm navigateToCategory is deployed on your site.
No canonical key = ... log on filter clickThe snippet didn't load. Check the snippet is in <head> and fast-serp-ready fires (check the network panel for fast-serp-ready event).
canonical key prints but doesn't match the mapCopy the printed key verbatim into your map. Facet names and values are case-insensitive in the snippet, but spaces and punctuation matter.
Redirect fires but lands on wrong collectionWrong categoryID in the map — re-check Step 3.
Redirect fires but URL still has filtersThe customization-side cleanup runs after 250ms. If it's still wrong after 1 second, set DEBUG=true and check URL cleaned → log.
Redirect fires twice or in a loopThe snippet has built-in cooldown + per-page max guards. Should not loop in normal use. If it does, post the fs-custom-events-navigate-to-category log output to support.
Skipped event with guard: "search-mode"You're on a search page (?q=). Feature is collection-only by design.
Skipped event with guard: "preview-mode"A merchandising preview is active. Feature is disabled in previews so previews show real behavior.

Customization patterns

The snippet supports two map shapes side-by-side. Pick whichever fits your scenario; you can use one, the other, or both at once.

Pattern A — Flat map (simplest)

Use this when each combo redirects to the same target regardless of which source category the shopper is on. Just write {combo → target} pairs. Optional: add an allow-list of source paths.

var FACET_COMBO_MAP = {
"color:blue|size:large": { categoryID: "...", categoryURL: "/collections/blue-large-shirts" },
"color:blue": { categoryID: "...", categoryURL: "/collections/blue-shirts" }
};

// Optional: only run on these source paths. [] = run on every PLP.
var SOURCE_PATHS = ["/collections/shirts"];

var FACET_COMBO_MAP_BY_SOURCE = {}; // empty — no per-source rules

Pattern B — Per-source map (different targets per source)

Use this when the same combo should redirect to different targets depending on which source category the shopper is on. Outer key = source path (prefix-matched against location.pathname); inner key = canonical combo.

var FACET_COMBO_MAP = {};            // empty — no global rules
var SOURCE_PATHS = [];

var FACET_COMBO_MAP_BY_SOURCE = {
"/collections/collection1": {
"size:8x10": { categoryID: "...", categoryURL: "/collections/category2" }
},
"/collections/collection3": {
"size:8x10": { categoryID: "...", categoryURL: "/collections/category4" }
}
};

In this example, picking Size: 8x10 on collection1 redirects to category2, while the same Size: 8x10 on collection3 redirects to category4.

Pattern C — Both at once (power users)

Mix both maps. Per-source overrides win for source-specific scenarios; the flat map handles the rest.

var FACET_COMBO_MAP = {
"color:red": { categoryID: "...", categoryURL: "/collections/red" }
};
var SOURCE_PATHS = ["/collections/shirts", "/collections/dresses"];

var FACET_COMBO_MAP_BY_SOURCE = {
"/collections/shirts": {
"color:red|size:large": {
categoryID: "...",
categoryURL: "/collections/red-large-shirts" // overrides the flat map for this specific source+combo
}
}
};

Lookup precedence

For each filter click, the snippet:

per-source match (FACET_COMBO_MAP_BY_SOURCE, longest prefix wins)
→ flat map match (FACET_COMBO_MAP, gated by SOURCE_PATHS)
→ no redirect (normal narrow flow)

JS Code

<script>
(function () {
/* =========================================================================
CUSTOMIZE — fill in whichever map(s) you need; leave the rest empty
========================================================================= */

// Flat map — combos that should redirect on any source page in SOURCE_PATHS.
// Use this when one combo always goes to the same target.
// Key format rules:
// - All lowercase
// - Multiple facets joined by "|"
// - Each pair is "facetname:value"
// - Pairs sorted alphabetically (so "color:blue" comes before "size:large")
// - Multi-value within a single facet: values sorted alphabetically and joined with "+"
//
// categoryID comes from the Fast Simon dashboard (Catalog → Collections →
// click the collection → copy the ID). Same on every platform.
//
// categoryURL is the storefront URL of the target collection on YOUR
// platform. The path conventions vary:
// Shopify → /collections/blue-large-shirts
// BigCommerce → /blue-large-shirts/ or /categories/blue-large-shirts
// Magento → /blue-large-shirts.html
// Wix / custom → whatever your storefront uses
// Use absolute paths (start with "/").
var FACET_COMBO_MAP = {
// "color:blue|size:large": { categoryID: "...", categoryURL: "/collections/blue-large-shirts" }
};

// Optional source-path allow-list for the flat map. [] = match anywhere.
// Examples by platform:
// Shopify: ["/collections/shirts"]
// BigCommerce: ["/shirts/"]
// Magento: ["/shirts.html"]
var SOURCE_PATHS = [];

// Per-source map — combos whose target depends on which source page the
// shopper is on. Outer key = source path (prefix-matched). Use this when
// the SAME combo should redirect to DIFFERENT targets depending on source.
// Wins over the flat map when both have the same canonical key.
var FACET_COMBO_MAP_BY_SOURCE = {
// "/collections/collection1": {
// "size:8x10": { categoryID: "...", categoryURL: "/collections/category2" }
// },
// "/collections/collection3": {
// "size:8x10": { categoryID: "...", categoryURL: "/collections/category4" }
// }
};

// While testing, set true → console logs every match attempt and reason.
var DEBUG = false;

// Defer the narrow read by one macrotask. Required because
// `fs-custom-events-filter-clicked` fires before the narrow store update
// microtask completes. 10 ms gives a safety margin on slow devices.
var DEFER_LISTENER_MS = 10;

// Schedule a `history.replaceState` cleanup after this delay so the URL
// ends clean even if a reactive block in the storefront re-pushes URL with
// a stale narrow snapshot. 250 ms outlasts any reasonable race.
var URL_CLEANUP_MS = 250;

/* =========================================================================
SHARED INFRASTRUCTURE
========================================================================= */

// Facets to ignore when building the canonical key. The Categories facet
// represents the source collection itself, not a filter combination.
var IGNORED_FACETS = { "categories": 1, "category": 1 };

function log() {
if (!DEBUG) return;
try { console.log.apply(console, ["[FastSimon redirect]"].concat([].slice.call(arguments))); } catch (_) {}
}

// Build a canonical lookup key from the current narrow.
// Example: { Size: Set(["Large"]), Color: Set(["Blue"]) }
// → "color:blue|size:large"
function canonicalize(narrow) {
if (!narrow || typeof narrow !== "object") return "";
return Object.keys(narrow)
.filter(function (k) { return !IGNORED_FACETS[String(k).toLowerCase()]; })
.map(function (k) {
var raw = narrow[k];
var values = (raw && typeof raw[Symbol.iterator] === "function")
? Array.from(raw)
: (raw == null ? [] : [raw]);
values = values.map(String).map(function (v) { return v.toLowerCase(); });
values.sort();
return String(k).toLowerCase() + ":" + values.join("+");
})
.sort()
.join("|");
}

// Find the per-source map whose path is the longest prefix of the current
// location. Longest-prefix wins — "/collections/foo/sub" picks "/collections/foo/sub"
// over "/collections/foo" if both are configured.
function findSourceMap(currentPath) {
var bestMap = null;
var bestLen = -1;
for (var sourcePath in FACET_COMBO_MAP_BY_SOURCE) {
if (!Object.prototype.hasOwnProperty.call(FACET_COMBO_MAP_BY_SOURCE, sourcePath)) continue;
if (currentPath.indexOf(sourcePath) === 0 && sourcePath.length > bestLen) {
bestMap = FACET_COMBO_MAP_BY_SOURCE[sourcePath];
bestLen = sourcePath.length;
}
}
return bestMap;
}

function isInSourcePathsAllowList(currentPath) {
if (!SOURCE_PATHS || !SOURCE_PATHS.length) return true;
for (var i = 0; i < SOURCE_PATHS.length; i++) {
if (currentPath.indexOf(SOURCE_PATHS[i]) === 0) return true;
}
return false;
}

function findTarget(narrow) {
var key = canonicalize(narrow);
if (!key) return null;
log("canonical key =", JSON.stringify(key));

var path = (window.location && window.location.pathname) || "";

// 1. Per-source lookup wins (most specific).
var sourceMap = findSourceMap(path);
if (sourceMap && sourceMap[key]) {
log("per-source match");
return sourceMap[key];
}

// 2. Flat map (gated by SOURCE_PATHS allow-list).
if (FACET_COMBO_MAP[key] && isInSourcePathsAllowList(path)) {
log("flat map match");
return FACET_COMBO_MAP[key];
}

return null;
}

// Customization-side URL cleanup. Some storefronts have reactive blocks
// that re-push URL when categoryID/categoryURL/narrow change. If they fire
// with a stale narrow snapshot (e.g. the user's pre-redirect filters), the
// URL ends with the source filters re-appended on the target collection.
// Using replaceState (not pushState) ensures we don't add an extra history
// entry — we just correct the current one.
function scheduleURLCleanup(target) {
setTimeout(function () {
try {
var clean = window.location.origin + target.categoryURL;
var stillOnTarget =
window.location.pathname === target.categoryURL ||
window.location.pathname === target.categoryURL.replace(/\/$/, "");
if (!stillOnTarget) { log("URL cleanup skipped — user navigated away"); return; }
if (window.location.href !== clean) {
history.replaceState(history.state || {}, "", clean);
log("URL cleaned →", clean);
}
} catch (e) {
console.warn("[FastSimon redirect] URL cleanup failed:", e);
}
}, URL_CLEANUP_MS);
}

function attemptRedirect(narrow, source) {
try {
var sopt = window.SerpOptions;
if (!sopt || typeof sopt.navigateToCategory !== "function") return;
if (sopt.getQuery && sopt.getQuery()) { log(source, "search mode; skip"); return; }

var target = findTarget(narrow);
if (!target) { log(source, "no map entry; skip"); return; }

log(source, "MATCH — navigating to", target.categoryURL, "(id:", target.categoryID + ")");
var result = sopt.navigateToCategory({
categoryID: target.categoryID,
categoryURL: target.categoryURL,
clearNarrow: true,
reason: "facet-combo (" + source + ")"
});
log(source, "navigateToCategory result:", result);
if (result && result.ok) scheduleURLCleanup(target);
} catch (e) {
console.warn("[FastSimon redirect] listener error in", source, ":", e);
}
}

function start() {
var sopt = window.SerpOptions;
if (!sopt || typeof sopt.navigateToCategory !== "function") {
console.warn("[FastSimon redirect] navigateToCategory missing — feature disabled.");
return;
}
log("ready. flat keys =", Object.keys(FACET_COMBO_MAP).length,
"; per-source sources =", Object.keys(FACET_COMBO_MAP_BY_SOURCE).length);

// Path 1: Fast Simon native filter drawer (the side panel "All Filters").
// Defer with setTimeout so getNarrow() reflects the post-click state.
window.addEventListener("fs-custom-events-filter-clicked", function () {
setTimeout(function () {
attemptRedirect(sopt.getNarrow ? sopt.getNarrow() : {}, "native-drawer");
}, DEFER_LISTENER_MS);
});

// Path 2: Any custom UI (merchant-built top-filter tabs, bespoke filter
// panels, etc.) that applies narrow by dispatching `send-fast-simon-params`
// on document. Skip this listener if your storefront has no such custom UI.
document.addEventListener("send-fast-simon-params", function (e) {
if (e && e.detail && e.detail.narrow !== undefined) {
attemptRedirect(e.detail.narrow, "top-tabs");
}
});
}

if (DEBUG) {
window.addEventListener("fs-custom-events-navigate-to-category", function (e) {
log("→ navigated:", e && e.detail);
});
window.addEventListener("fs-custom-events-navigate-to-category-skipped", function (e) {
log("× skipped:", e && e.detail);
});
}

// Wait for fast-serp-ready (it fires after Fast Simon installs SerpOptions).
// Also try once immediately in case the event already fired.
window.addEventListener("fast-serp-ready", start, { once: true });
if (window.SerpOptions && typeof window.SerpOptions.navigateToCategory === "function") {
start();
}
})();
</script>

How the matching works

Each filter click on a source category triggers the snippet to:

  1. Read the current narrow (the set of selected filters) from window.SerpOptions.getNarrow().
  2. Build a canonical key from the narrow — pairs lowercased, alphabetically sorted, multi-value joined with +.
  3. Look up the canonical key in FACET_COMBO_MAP_BY_SOURCE first (longest source prefix wins). If found → call navigateToCategory(...).
  4. Otherwise fall back to FACET_COMBO_MAP (gated by SOURCE_PATHS). If found → call navigateToCategory(...).
  5. Schedule a history.replaceState cleanup ~250 ms later as a belt-and-suspenders to guarantee the URL ends clean.

For example, if the shopper picks Size: Large then Color: Blue (in any order), the snippet builds the canonical key "color:blue|size:large" (alphabetical order) and finds the matching entry in the map — regardless of the order the shopper clicked the filters.

Validating canonical keys

The map keys must match exactly what canonicalize() produces from your storefront's narrow object. Set DEBUG = true, click each filter once on the source category, and read the console:

[FastSimon redirect] canonical key = "color:blue"
[FastSimon redirect] canonical key = "color:blue|size:large"

If the printed key doesn't match what you expected, copy the printed key verbatim into your map. The most common mismatch is the facet name itself — for example, your storefront may emit "colors" (plural) instead of "color", or use a localized label.

Companion events

The snippet logs (with DEBUG = true):

  • fs-custom-events-navigate-to-category — fires on a successful redirect with {from, to, reason}
  • fs-custom-events-navigate-to-category-skipped — fires when a guard rejects the call with {error, guard, reason} (helpful for debugging which guard tripped)

See Custom Events for the full event reference.

Disabling the feature

Set window.__fast_options.disable_navigate_to_category = true to hard-disable navigateToCategory for the site. See Options for related configuration.