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:
navigateToCategoryhas 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:
| Platform | Where to put it |
|---|---|
| Shopify | theme.liquid <head>, or a snippet file included via {% include 'fastsimon-navigate-to-category' %} |
| BigCommerce | Storefront > Script Manager (Footer or Head; the snippet auto-waits for fast-serp-ready) |
| Magento | A custom layout XML or theme <head> block |
| Wix / Custom | <head> of the page template, or any global scripts injection point |
| Hosted/SaaS | Tag 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 category | Combo (filter values) | Target category |
|---|---|---|
| Shirts | Color: Blue + Size: Large | Blue Large Shirts |
| Shirts | Color: Blue | Blue Shirts |
| Dresses | Color: Red | Red 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).
- Open the Fast Simon dashboard.
- Go to Catalog → Collections.
- Find each target collection by name.
- 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:
| Platform | URL path example |
|---|---|
| Shopify | /collections/blue-large-shirts |
| BigCommerce | /blue-large-shirts/ |
| Magento | /blue-large-shirts.html |
| WooCommerce | /product-category/blue-large-shirts/ |
| Custom | whatever 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:
- Choose the right map shape for your pattern (the Customization patterns section below shows what each shape looks like).
- 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). - Set
DEBUG = truefor now — you'll flip it back tofalseafter 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:
- In your Shopify admin, go to Online Store → Themes → … → Edit code.
- Open
theme.liquid. - Find the line that loads the Fast Simon script (usually inside
<head>). - Paste the snippet immediately after it.
- 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
- Open your storefront in a browser.
- Open DevTools → Console (F12).
- Visit your source category (e.g.,
https://yourstore.com/collections/shirts). - Look for
[FastSimon redirect] ready. ...— confirms the snippet loaded. - Click each filter you care about, one at a time. After each click, the console should print:
[FastSimon redirect] canonical key = "color:blue" - 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. - 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). - Verify the redirect lands on the correct collection with the correct products.
- 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:
- Set
DEBUG = falsein the snippet (otherwise every shopper's console fills with logs). - Save the theme.
- 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
| Symptom | Likely cause |
|---|---|
Console shows navigateToCategory missing — feature disabled | Your 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 click | The 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 map | Copy 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 collection | Wrong categoryID in the map — re-check Step 3. |
| Redirect fires but URL still has filters | The 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 loop | The 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:
- Read the current narrow (the set of selected filters) from
window.SerpOptions.getNarrow(). - Build a canonical key from the narrow — pairs lowercased, alphabetically sorted, multi-value joined with
+. - Look up the canonical key in
FACET_COMBO_MAP_BY_SOURCEfirst (longest source prefix wins). If found → callnavigateToCategory(...). - Otherwise fall back to
FACET_COMBO_MAP(gated bySOURCE_PATHS). If found → callnavigateToCategory(...). - Schedule a
history.replaceStatecleanup ~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.