Developers / Where-to-buy / Install
Install the where-to-buy widget
Three ways in: a script tag for any website, a plain iframe, or the React component. To pick colors and copy a ready-made snippet, use the live demo.
Any website
Paste this where the locator should appear. The script swaps the div for the widget in a frame that resizes to fit, so there's no scrollbar and no gap.
<div data-fuzzy-embed="where-to-buy" data-brand="your-brand-slug"></div>
<script src="https://www.fuzzy.so/embed/where-to-buy.js" async></script>With options:
<div data-fuzzy-embed="where-to-buy"
data-brand="your-brand-slug"
data-theme="dark"
data-accent="#7c3aed"
data-radius="16"
data-font="DM Sans"
data-product="PRODUCT_ID"></div>
<script src="https://www.fuzzy.so/embed/where-to-buy.js" async></script>| Attribute | What it does |
|---|---|
data-brand | Required. Your brand's slug, as in fuzzy.so/developers/where-to-buy/{slug}. |
data-theme | light (default), dark, or auto (follows the shopper's device). |
data-accent | Hex color for buttons and links, e.g. #0a6e4d. |
data-radius | Corner radius in px, 0 to 24. Default 10. |
data-font | A Google Font name from the picker on the demo page, e.g. Inter. |
data-density | compact for tighter cards. |
data-transparent | Drop the widget's background so your page shows through. |
data-state | Two-letter state to limit stores to, e.g. MI. |
data-title / data-products / data-filters / data-states / data-location / data-map / data-credit | Set to "0" to hide the heading, product search, quick filter chips, ZIP box, store map, or the Powered by fuzzy credit. |
data-product | A product id: adds that product's schema.org markup to your page (see SEO). |
data-site | Attribution. Defaults to your page's hostname. |
Several widgets on one page are fine. For a page builder that adds blocks after load, call window.FuzzyWhereToBuy.mount() once the block is in place.
WordPress
The plugin prints the store list in your page as plain HTML, so search engines and AI answer engines read it as your page's own content. One install covers every site you run; each page names its brand in the shortcode.
- Download the plugin and add it under Plugins → Add New → Upload Plugin.
- Put the shortcode on any page, or drag the Where to buy (fuzzy) widget in Elementor.
[fuzzy_where_to_buy brand="your-brand-slug"]One template, many pages: a page per state, per town or per product, each with its own real store names and addresses.
[fuzzy_where_to_buy brand="your-brand-slug" state="NJ"]
[fuzzy_where_to_buy brand="your-brand-slug" city="Jersey City" state="NJ"]
[fuzzy_where_to_buy brand="your-brand-slug" product="product-id" finder="1"]
[fuzzy_products brand="your-brand-slug" state="NJ"]
[fuzzy_faq brand="your-brand-slug" state="NJ"]brand- Your brand slug (required).
state / city- List one state's or one town's stores: a page per town.
product- Only stores carrying one product, from products/top: a page per product. The list then says what it sells for and where, with a schema.org Product.
limit- Stores to list, 1 to 100. Default 100.
title- Heading override.
theme / accent / radius / density- The same look options as the script.
finder- "1" adds the interactive finder (town, ZIP, Near me, map) under the list.
match- A product by the name your page uses ("Choco Chunk Cookie"), matched to the menus, in place of product. Prints nothing when no store carries it.
faq- "1" adds common questions under the list (see [fuzzy_faq]).
prices / counts- "0" hides prices (store prices, price ranges, and the price in search markup) or how many stores carry it. Both show by default; set them for the whole site under Settings → fuzzy where to buy.
questions- Which questions [fuzzy_faq] answers, in order: any of "where,price,products,towns". Default all.
[fuzzy_products]- Your products instead of your stores: each with its shelf price range and how many dispensaries carry it, with schema.org Product markup. Takes brand, state, limit, title and the look options. In Elementor, set the widget's Show to Products.
[fuzzy_faq]- Questions shoppers ask, answered from live menus: where to buy, how much, the most-carried products and the towns with the most stores, with schema.org FAQPage markup. Takes brand, state, match, title and the look options.
Every product page, with no shortcode. On a WooCommerce site the plugin offers, once, to add where to buy to every product page. Each page's title is matched to the menus, and the page gets the dispensaries that carry that product, what it sells for, its Product markup and common questions. A page whose product no store carries is left as it is, and a new product page gets its list the day it is published. Settings → fuzzy where to buy turns it on or off.
Each page remembers when its list last changed (a store added, a price moved), and its date in the sitemap (WordPress's, Yoast's or Rank Math's) follows, so search engines come back when the menus change.
Your server fetches the list from fuzzy and keeps it for six hours. After that the page shows the last good copy at once while WordPress fetches a fresh one in the background, so visitors and crawlers never wait on fuzzy. When fuzzy can't be reached it keeps showing the last good copy, so the page never goes blank. Visitors' browsers never contact fuzzy for the list. The list ends with the Powered by fuzzy logo, a plain link to fuzzy.so: the plugin is free, and that credit is the price.
Or, without the plugin, the interactive widget in a Custom HTML block:
- Edit the page where shoppers should find stores.
- Add a block with +, search for Custom HTML, and add it.
- Paste the script snippet from the demo page (or above) and publish.
The same goes for Squarespace (a Code block), Webflow (an Embed element), Wix (Embed HTML) and Shopify (a Custom Liquid section). If your site builder strips scripts, use the iframe below.
iframe
For a site that strips scripts. It can't resize itself, so give it a height that suits your page. Keep allow="geolocation" or Near me won't work inside the frame.
<iframe
src="https://www.fuzzy.so/embed/where-to-buy/your-brand-slug?theme=light"
title="Where to buy, powered by fuzzy"
allow="geolocation"
loading="lazy"
style="width:100%;min-height:720px;border:0;"
></iframe>It takes the same options as query parameters: theme, accent (hex without the #), radius, font, density, transparent=1, state, and title/products/filters/states/location/map/credit set to 0 to hide.
React
The component renders in your page rather than a frame, so it can take on your site's font and colors (appearance="inherit", the default) and its schema.org markup belongs to your page. Peer dependency react >= 18; no CSS import, no Tailwind.
npm install @fuzzylabs/where-to-buy
import { WhereToBuy } from "@fuzzylabs/where-to-buy";
export function StoreLocator() {
return (
<WhereToBuy
brand="your-brand-slug"
brandName="Your Brand"
site="yourbrand.com"
/>
);
}With the store map
The map is a separate entry point so the core package stays small: install maplibre-gl (4.x or 5.x) and pass StoreMap. Wide layouts show the list and map side by side; narrow ones get a List/Map toggle. In Next.js, load it with next/dynamic and ssr: false, since MapLibre needs the browser.
npm install @fuzzylabs/where-to-buy maplibre-gl
import { WhereToBuy } from "@fuzzylabs/where-to-buy";
import { StoreMap } from "@fuzzylabs/where-to-buy/map";
<WhereToBuy brand="your-brand-slug" map={StoreMap} />| Prop | Type | Default | What it does |
|---|---|---|---|
brand | string | required | Brand slug, e.g. "heavy-hitters". |
brandName | string? | — | Display name for the heading ("Where to buy …"). No heading without it. |
title | string | false | — | Heading override; false hides it. |
appearance | "inherit" | "light" | "dark" | "auto" | "inherit" | inherit blends into your page's font and colors. |
accent | string? | "#0a6e4d" | Buttons, links and focus ring. |
cornerRadius | number? | 10 | Corner radius in px. |
font | string? | your font | CSS font-family. |
density | "comfortable" | "compact" | "comfortable" | Card spacing. |
locale | string? | shopper's | Number, price and date formatting, e.g. "en-US". |
showLocation | boolean? | true | ZIP box and Near me button. |
allowGeolocation | boolean? | true | Offer Near me. |
zip / near | string? / {lat,lng}? | — | Start sorted from a ZIP or a point. |
radiusMiles | number? | — | Only stores within this distance. Default: all, nearest first. |
rememberLocation | boolean? | true | Remember the shopper's last ZIP in localStorage on your domain. |
pageSize | number? | 10 | Stores per page, up to 100. |
showProductPicker | boolean? | true | Product search; picking one links each store to that product. |
showQuickFilters | boolean? | true | One-tap chips above the search box: All plus the brand's top products. Follows showProductPicker. |
showStateFilter | boolean? | true | For a brand sold in more than one state: All states plus a chip per state (a select past six). Hidden when state is set. |
includeJsonLd | boolean? | true | Inject schema.org JSON-LD for the picked product. |
showAttribution | boolean? | true | The small Powered by fuzzy credit: our logo, linking to fuzzy.so. |
state / city | string? | — | Limit stores to a state or city. |
site | string? | — | Your domain, for attribution. |
onStoreLinkClick | (store, link) => void | — | Fires on a menu or directions click, for your analytics. |
staleAfterDays | number? | 7 | Past this age a row says when it was last seen instead of claiming stock. |
clientOptions | ClientOptions? | — | Override the API baseUrl or the fetcher. |
map | StoreMapComponent? | — | StoreMap from @fuzzylabs/where-to-buy/map. No map without it. |
showMap / mapHeight | boolean? / number? | true / 620 | Hide the map, or set its height in px. |
style / className | — | — | On the root; style takes the --fuzzy-* variables. |
Source of truth: packages/where-to-buy/src/WhereToBuy.tsx's WhereToBuyProps.
SEO and schema.org
For one product, the widget can add schema.org Product markup with an AggregateOffer listing the stores that carry it. Search engines can use it for rich results. How much of it they see depends on how you embed:
| Embed | SEO value | Why |
|---|---|---|
| Server-rendered list (WordPress plugin, React server component) | Best | Every store is real text in your page's HTML (name, address, phone, what it carries), with schema.org ItemList markup, so every crawler and AI answer engine reads it as your content. Refreshed every six hours, no JavaScript needed. |
| React, server-rendered markup | Best | The JSON-LD is in your page's HTML, so every crawler sees it. Needs a server-rendering React setup (Next.js and similar) and one product per page. |
| React, default | Good | The widget adds the markup in the browser once a shopper picks a product. Google renders JavaScript and usually reads it; other crawlers and link previews may not. |
| Script with data-product | Partial | The script adds that one product's markup to your page's <head> from JavaScript, with the same caveat as above. The store list itself lives in the iframe, so it doesn't count as content on your page. Without data-product, no markup is added. |
| Credit link (script and React) | Backlink | The Powered by fuzzy logo is a plain, followed link on your page, so it counts as a link to fuzzy.so. The widget is free; the credit is how it pays for itself, so please leave it on. The plain iframe keeps its credit inside the frame, which passes no link: add the line below the iframe yourself if you want it to. |
| iframe | None | Markup inside a frame describes the frame, not your page, and the frame is marked noindex. Pick this only when your site won't take a script. |
The server-rendered store list for a custom React site, the same HTML the WordPress plugin prints:
// app/where-to-buy/page.tsx (a React Server Component)
import { WhereToBuyList, fetchWhereToBuy } from "@fuzzylabs/where-to-buy/static";
export const revalidate = 21600; // six hours, about how often menus are read
export default async function Page() {
const data = await fetchWhereToBuy("your-brand-slug", { state: "NJ", limit: 100 });
return <WhereToBuyList data={data} brandName="Your Brand" placeName="New Jersey" />;
}The interactive widget with server-rendered product markup:
// app/where-to-buy/[product]/page.tsx (Next.js App Router)
import { fetchWhereToBuyJsonLd } from "@fuzzylabs/where-to-buy/client";
import { StoreLocator } from "./StoreLocator"; // a "use client" file rendering <WhereToBuy includeJsonLd={false} />
export default async function Page({ params }: { params: Promise<{ product: string }> }) {
const { product } = await params;
const jsonLd = await fetchWhereToBuyJsonLd("your-brand-slug", product).catch(() => null);
return (
<>
{jsonLd ? (
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
) : null}
<StoreLocator />
</>
);
}Product ids come from GET https://api.fuzzy.so/api/radar/brands/{slug}/products/top (productClusterId), or the useTopProducts hook.
Theming
The props cover most of it. For anything finer, the widget reads these CSS variables, and every card, button and row carries a stable data-fuzzy-* attribute you can target from your own CSS.
<WhereToBuy
brand="your-brand-slug"
style={{
"--fuzzy-accent": "#0a5", // buttons, links, focus ring
"--fuzzy-on-accent": "#fff", // text on accent buttons
"--fuzzy-text": "#1a1a1a",
"--fuzzy-muted": "#6b7280",
"--fuzzy-border": "#e5e7eb",
"--fuzzy-card-bg": "#fff", // light / dark / auto
"--fuzzy-bg": "#fafafa", // light / dark / auto
} as React.CSSProperties}
/>Near me and privacy
- A ZIP is matched to its Census ZIP area and stores sort by straight-line distance from its center. Directions open Google Maps.
- Near me asks the browser for a location only when the shopper taps it, and sends it rounded to about 100 meters. Nothing is asked on page load.
- No cookies and no shopper identifiers. The React component remembers the last ZIP in
localStorageon your own domain (rememberLocation=falseto turn that off). - The store map's tiles come from OpenFreeMap, so a shopper's browser requests them from OpenFreeMap's servers (no key, no cookies). Hide the map to avoid that request.
- Requests carry your site's hostname and the kit version so usage can be attributed per site. That's all.
Headless and REST
Build your own markup on the same data with the hooks, or call the endpoint from anything. It's public, CORS-open, and needs no key.
import { useWhereToBuy } from "@fuzzylabs/where-to-buy";
function Stores({ brand }: { brand: string }) {
const { data, loading, error } = useWhereToBuy(brand, { zip: "80216" });
if (loading || error) return null;
return (
<ul>
{data?.stores.map((s) => (
<li key={s.doorKey}>
{s.doorName ?? s.retailerName}, {s.distanceMiles} mi
{s.deepLink && <a href={s.deepLink}>View menu</a>}
</li>
))}
</ul>
);
}curl "https://api.fuzzy.so/api/radar/brands/your-brand-slug/where-to-buy?zip=80216&limit=5"Parameters: zip or lat+lng (nearest first), radius (miles), product, state, city, page, limit (max 100), site. Full reference on the REST API page.
For agents: full setup details+ expand− collapse
Package
- npm:
@fuzzylabs/where-to-buy(MIT). Peer dependencyreact >= 18; no other runtime dependencies. - Package and README: npmjs.com/package/@fuzzylabs/where-to-buy
- Embed script:
https://www.fuzzy.so/embed/where-to-buy.js. Frame:https://www.fuzzy.so/embed/where-to-buy/{slug}.
Exports
<WhereToBuy brand brandName? appearance? accent? .../> styled locator
useWhereToBuy(brand, { zip?, lat?, lng?, radius?, product?, state?, page?, limit? })
useTopProducts(brand, opts?) hook over /products/top
<LocationControl />, <StoreList />, <StoreCard />, <Pagination />, <ProductSearch />
<WhereToBuyJsonLd brand product /> schema.org JSON-LD only
fetchWhereToBuy / fetchTopProducts / fetchWhereToBuyJsonLd plain fetch, no React
formatCount, formatDistance, storeName, storeAddress, directionsUrl display helpersResponse row (where-to-buy)
{ doorKey, retailerName, retailerSlug, doorName, doorCity, city, state,
address, postalCode, lat, lng, distanceMiles,
platformSlug, deepLink, deepLinkKind: "menu" | "search",
price, inStock, productCount, lastSeenAt }
+ top level: { stores, total, page, limit, near: { lat, lng, zip } | null }Data limits
- Coverage is live dispensary menus on Dutchie and Jane nationwide, plus Dispense in New Jersey. Not Leafly, not a store's own site. A door count is a floor, not a census.
- “Current” means the last completed scrape of that retailer, not a point-of-sale feed. Scrapes run on their own cadence, not a fixed day.
lastSeenAtis when a listing was last scraped present.inStock: nullmeans the platform does not expose inventory; we never guess a number.- Issue rates are 0–1 fractions, not percentages. Figures drift with every scrape, so re-pull before quoting one.