← back to Fix Live Board
config/showroom-vendor.cjs
93 lines
/**
* showroom-vendor.cjs — canonical `isShowroomVendor(vendor)` primitive. (TK-11186)
*
* Showroom-only vendors are "addressable but not discoverable": reachable via a
* direct /products/<handle> URL or on-site search, but HIDDEN from every push /
* browse surface and the Google feed. The one source of truth for WHICH vendors
* are showroom-only is the sibling `showroom-vendors.json` in this directory
* (currently ["Phillip Jeffries"]). NEVER hardcode a vendor name anywhere else —
* add/remove vendors by editing that JSON file only.
*
* Lives beside the list on purpose: one directory owns the showroom-vendor concept
* (list + logic). It is a plain CommonJS module so BOTH:
* - CommonJS callers: const { isShowroomVendor } = require('.../showroom-vendor.cjs')
* - ESM (.mjs) callers: import { createRequire } from 'node:module';
* const require = createRequire(import.meta.url);
* const { isShowroomVendor } = require('.../showroom-vendor.cjs');
* consume the SAME implementation and the SAME list.
*
* Reversible: delete this file / revert the commit. No side effects, read-only.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const LIST_PATH = path.join(__dirname, 'showroom-vendors.json');
// Case-insensitive exact-match set, read once and memoized. Resilient: a missing or
// malformed list yields an empty set (isShowroomVendor -> always false) rather than
// throwing, so a config hiccup can never crash a feed build or a fleet server boot.
let _set = null;
function _load() {
if (_set) return _set;
try {
const raw = JSON.parse(fs.readFileSync(LIST_PATH, 'utf8'));
const arr = Array.isArray(raw) ? raw : [];
_set = new Set(arr.map(v => String(v == null ? '' : v).trim().toLowerCase()).filter(Boolean));
} catch {
_set = new Set();
}
return _set;
}
/** True iff `vendor` is on the showroom-only list (case-insensitive exact match). */
function isShowroomVendor(vendor) {
if (vendor == null) return false;
return _load().has(String(vendor).trim().toLowerCase());
}
// The product-level showroom TAG. A SHARED-vendor line (e.g. MDC under the shared
// private label "Phillipe Romano", which also carries SELLABLE lines like Spazzolato/
// Greenland/RIGO/Justin David) cannot be suppressed by vendor name without hiding that
// vendor's sellable products. So a product is ALSO showroom-only if it carries this tag.
// TK-11307.
// TK-11307 (Steve, 2026-09-10): the tag is 'ShowroomOnly', NOT 'Showroom'. A pre-existing
// 'Showroom Line' tag sits on 18,518+ ACTIVE SELLABLE products, and Shopify/Boost `tag:`
// queries are PREFIX/TOKEN matchers (quoting does not help) — `tag:Showroom` returns those
// 18.5k sellable products and ZERO showroom products. 'ShowroomOnly' measures 0 collisions,
// so it is safe in code AND in a Shopify/Boost tag query. Compared lowercased.
const SHOWROOM_TAG = 'showroomonly';
/** Case-insensitive check: does this product's tags array/string include the ShowroomOnly tag? */
function hasShowroomTag(tags) {
if (tags == null) return false;
const arr = Array.isArray(tags) ? tags : String(tags).split(',');
return arr.some(t => String(t).trim().toLowerCase() === SHOWROOM_TAG);
}
/**
* True iff the PRODUCT is showroom-only (addressable-but-not-discoverable): EITHER its
* vendor is on the showroom-vendors.json list, OR it carries the product-level "ShowroomOnly"
* tag. The tag path is how a shared-vendor line is hidden from discovery/feed surfaces
* WITHOUT hiding the vendor's sellable products. Callers pass the whole product object
* (must include `vendor` and `tags`). Never hardcode a vendor name — edit the JSON list
* for vendor-level, apply the "ShowroomOnly" tag for product-level. TK-11307.
*/
function isShowroomProduct(product) {
if (!product) return false;
if (isShowroomVendor(product.vendor)) return true;
return hasShowroomTag(product.tags);
}
/** The raw showroom-vendor names, as authored in showroom-vendors.json. */
function showroomVendors() {
try {
const raw = JSON.parse(fs.readFileSync(LIST_PATH, 'utf8'));
return Array.isArray(raw) ? raw.slice() : [];
} catch {
return [];
}
}
module.exports = { isShowroomVendor, isShowroomProduct, hasShowroomTag, showroomVendors, LIST_PATH };