Taming Tag-Driven Logic in a Gated Shopify B2B Store
API Fishcare's Shopify store serves two audiences off customer tags that were never spelled the same way twice. Here's how I normalised them, split audience from spend, and kept the cart honest with checkout.
API Fishcare runs a gated Shopify store on an old Debut theme. You don't see anything without an approved account, and once you're in there are two separate catalogues: Store Use for retailers and Home Use for hobbyists. Which one you get depends on a tag on your customer record.
Most of my work on it this summer came down to one problem: the theme was making business decisions from tags, and the tags weren't consistent. Here's what I built to deal with that, and the patterns I'd reuse.
Three spellings of one tag
Over the years the store had picked up storeuse, Store Use and store-use, all meaning the same thing. Liquid's contains on a tag array is an exact, case-sensitive match. So a customer tagged storeuse didn't show up for any check looking for Store Use. Nothing threw an error. The page just behaved as if the customer had no tag.
I could have picked one spelling and re-tagged the whole catalogue, but somebody would add a fourth spelling within the month. So I wrote one snippet, store-use-flags, that normalises every tag before comparing it:
{%- for sufl_tag in customer.tags -%}
{%- assign sufl_t = sufl_tag | downcase | replace: ' ', '' | replace: '-', '' | replace: '_', '' -%}
{%- if sufl_t contains 'store' -%}{%- assign customer_is_store_use = true -%}{%- endif -%}
{%- if sufl_t contains 'homeuse' -%}{%- assign customer_is_home_use = true -%}{%- endif -%}
{%- endfor -%}
Every template that used to test tags inline now includes this snippet and reads the booleans. That covers product access, the header, the cart, the collection grids and the welcome-gift script.
The customer match started out as contains 'storeuse'. In September I loosened it to contains 'store', because retail accounts were also being tagged things like STORE - FW ONLY and STORE - SW ONLY, and those customers weren't registering as Store Use. I only loosened it on the customer side. Product matching still looks for storeuse.
One normalising snippet beats any number of re-tagging sessions. It also covers spellings nobody has typed yet.
Audience and spend are different questions
The part that took the most thought was working out that "is this a Store Use product?" is really two questions.
Take a product tagged AQUASPIN - Store Use. Normalised, that's aquaspinstoreuse, which contains storeuse, so Store Use customers should see it and Home Use customers shouldn't. That's the audience question, and a substring match is right for it.
The same product should not count toward the store-use spend threshold for free shipping. That's the spend question, and it needs an exact match:
{%- if sufl_t contains 'storeuse' -%}{%- assign product_is_store_use = true -%}{%- endif -%}
{%- if sufl_t == 'storeuse' -%}{%- assign product_is_store_use_spend = true -%}{%- endif -%}
The snippet sets both flags. The comment at the top says which is which: use product_is_store_use for visibility and badges, and product_is_store_use_spend for anything that adds up money. When one flag is answering two questions, split it before it causes a billing bug.
Don't show people a door they can't open
product-access already stopped a Home Use customer from seeing a Store Use product page. But the nav still linked to Store Use collections, and the collection grids still showed their tiles. A Home Use customer could click through and land on a page full of "not available for your customer type."
I fixed that in two places.
The header picks a menu once. Home Use customers get a separate menu, chosen in the theme editor, with a fallback to a home-use handle. The header works that out once at the top and passes active_linklist to all four places Debut renders navigation: desktop, mobile, the mobile toggle's blank check, and the centred-logo variant. Before, each of those read the setting separately, and that's how desktop and mobile nav end up out of sync. The swap only happens if the Home Use menu exists and has links, so an empty menu can't blank the header.
Links and tiles get classified by their own text. audience-visibility takes a handle, title or URL, normalises it the same way, and hides it if it clearly belongs to the other audience. Anything that mentions both audiences, or neither, stays visible. It also fails open: if no customer flags are set, whether because the visitor is logged out or a caller forgot the include, everything shows. A menu with one link too many is a much smaller problem than an empty one.
The cart has to agree with checkout
The actual promotions are Shopify Functions in a companion app: a discount function for gift-with-purchase and a delivery customization that hides the free-shipping rate until the threshold is met. Functions only run at checkout, so the cart page had no idea any of it existed. It said nothing, or worse, it promised something checkout wouldn't honour.
So the theme now mirrors the Functions:
- Free-shipping progress bar. It adds up qualifying store-use lines using
original_line_price, because that's the pre-discount subtotal the Function sees. Using it means a discount code can't knock someone back under the threshold. Gift lines are left out. Home Use customers don't see the bar, because they can't buy anything that counts toward it. - Gift with purchase. Tagged by product, not in code.
gwp-machinemarks the product that triggers a gift andgwp-giftmarks what can be picked. An automated collection on each tag gives Liquid something to loop over. Marketing can change the gift line-up by re-tagging, with nothing to redeploy. The gift list is filtered by audience too, because both catalogues carry the same programme tags. - A kill switch. "Spend X, get a gift" used to be hard-coded in four snippets. Now
promo-configholds a couple of booleans and the threshold, and the comment beside each one names the setting it has to match in the app. That programme has since been retired. It went away with onefalse, and it comes back the same way.
Each of these snippets states its parity rules at the top: which tags, which threshold, and which file in the Functions app has to change along with it. The theme decides what the customer is told. Checkout decides what they're charged. Those two drifting apart is the bug worth guarding against.
The AJAX updates don't duplicate any of that logic in JavaScript. /cart.js doesn't expose product tags, so the browser can't do the maths anyway. The snippet re-fetches its own section through the Section Rendering API and swaps in the fresh HTML:
fetch('/?section_id=' + encodeURIComponent(sectionId), { credentials: 'same-origin' })
.then(function (res) { return res.text(); })
.then(function (html) {
var doc = new DOMParser().parseFromString(html, 'text/html');
var fresh = doc.querySelector('[data-fsg] [data-fsg-inner]');
if (fresh) root.querySelector('[data-fsg-inner]').innerHTML = fresh.innerHTML;
});
Debut makes every cart change through jQuery, so a single ajaxComplete listener that filters for /cart/ URLs is enough to trigger it, and theme.js didn't need touching.
Two smaller things
include, not render. All of these flag snippets set variables for the caller to read. render gives the snippet an isolated scope, so the flags never make it back, and Shopify won't allow include inside a rendered snippet at all. Every one of them has a warning about this in its header. It's legacy syntax, and in this theme it's also the right tool.
A staff-only debug strip. Tag chips used to show under the hero on the Store Use home page, to every logged-in customer, internal tags included. They now sit in a thin bar above the footer, visible only to accounts with an admin tag. The bar shows the resolved flags next to the raw tags. When something looks wrong, the useful question usually isn't "what tags does this account have?" It's "what did the theme decide they mean?"
None of this is glamorous. It's an old theme with business rules spread across tags. But the store now has a single source of truth for who a customer is, and the cart and checkout tell customers the same thing.
FREE Shopify Product Migration
Moving to Shopify? We'll migrate your product catalog for free. New stores only.