← back to All Designerwallcoverings
api: carry series (Book) facet + website/internal_url per product
3876a0da9322b58611fcda518734c876122eb700 · 2026-07-07 07:13:35 -0700 · Steve
Files touched
Diff
commit 3876a0da9322b58611fcda518734c876122eb700
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jul 7 07:13:35 2026 -0700
api: carry series (Book) facet + website/internal_url per product
---
server.js | 27 +++++++++++++++++++++++----
1 file changed, 23 insertions(+), 4 deletions(-)
diff --git a/server.js b/server.js
index 74247a0..ba0eb42 100644
--- a/server.js
+++ b/server.js
@@ -49,6 +49,16 @@ const priceBand = (p) => { if (!p || p <= 0) return null; const b = PRICE_BANDS.
const titleCase = (s) => s.replace(/\w\S*/g, (t) => t[0].toUpperCase() + t.slice(1));
+// Public storefront (custom domain) + internal Shopify admin store handle. The internal
+// per-SKU page is the Shopify admin product view, keyed by the product's numeric id
+// (extracted from the shopify_id GID). Public surface still ships only public-safe cols.
+const STOREFRONT = 'https://designerwallcoverings.com';
+const ADMIN_STORE = process.env.SHOPIFY_ADMIN_STORE || 'designer-laboratory-sandbox';
+// DW SKU series/book code = the leading alpha run of the SKU (DWTT, RML, GRS, XJG …).
+// Deterministic, in-RAM, and never leaks an upstream/private-label name (it's a house code).
+const seriesOf = (sku) => { const m = (sku || '').match(/^[A-Za-z]{2,}/); return m ? m[0].toUpperCase() : null; };
+const adminUrl = (gid) => { const m = (gid || '').match(/(\d+)\s*$/); return m ? `https://admin.shopify.com/store/${ADMIN_STORE}/products/${m[1]}` : null; };
+
// HARD front-facing rule (same list build-data.py enforced for the vendor directory):
// banned upstream/private-label source names never ship on this public surface — rows
// whose vendor OR title leaks one are excluded (their products surface under the label).
@@ -67,7 +77,7 @@ async function snapSql() {
const priceExpr = has.has('min_variant_price') ? 'coalesce(price, min_variant_price)' : 'price';
const pubExpr = has.has('online_store_published') ? 'online_store_published' : "(upper(coalesce(status,'')) = 'ACTIVE')";
return `
- SELECT id, handle, title, vendor, product_type, dw_sku, variant_sku, sku, pattern_name,
+ SELECT id, shopify_id, handle, title, vendor, product_type, dw_sku, variant_sku, sku, pattern_name,
tags, upper(coalesce(status,'')) AS status, image_url,
${priceExpr} AS price,
${pubExpr} AS online_store_published,
@@ -98,9 +108,11 @@ function deriveRow(r) {
: r.status === 'DRAFT' ? 'Staged'
: (r.status === 'ACTIVE' && r.online_store_published) ? 'Active on site'
: 'Staged';
+ const sku = r.dw_sku || r.variant_sku || r.sku || null;
return {
id: r.id,
- sku: r.dw_sku || r.variant_sku || r.sku || null,
+ sku,
+ series: seriesOf(sku),
title: r.title || '',
vendor: r.vendor || null,
type: r.product_type || null,
@@ -114,7 +126,10 @@ function deriveRow(r) {
styles: [...new Set(styles)],
materials: [...new Set(materials)],
image_state: r.image_url ? 'Has image' : 'No image',
- url: (r.online_store_published && r.handle) ? `https://designerwallcoverings.com/products/${r.handle}` : null,
+ // Website = customer-facing storefront page (only when actually live on the Online Store).
+ url: (r.online_store_published && r.handle) ? `${STOREFRONT}/products/${r.handle}` : null,
+ // Internal = Shopify admin product view (resolves for any row that has a shopify_id GID).
+ internal_url: adminUrl(r.shopify_id),
created: r.created_at_shopify ? new Date(r.created_at_shopify).getTime() : 0,
updated: r.updated_at_shopify ? new Date(r.updated_at_shopify).getTime() : 0,
// one lowercase haystack per row so search is a single .includes() pass
@@ -139,6 +154,7 @@ function parseFilters(u) {
return {
q: (u.searchParams.get('q') || '').trim().toLowerCase(),
vendors: csv('vendors'), types: csv('types'), statuses: csv('statuses'), lifecycles: csv('lifecycles'),
+ series: csv('series'),
colors: csv('colors'), styles: csv('styles'), materials: csv('materials'),
prices: csv('prices'), images: csv('images'),
};
@@ -158,6 +174,7 @@ function applyFilters(rows, f, opts = {}) {
}
if (opts.skip !== 'vendor' && f.vendors.size) out = out.filter((r) => r.vendor && f.vendors.has(r.vendor));
if (opts.skip !== 'type' && f.types.size) out = out.filter((r) => r.type && f.types.has(r.type));
+ if (opts.skip !== 'series' && f.series.size) out = out.filter((r) => r.series && f.series.has(r.series));
if (opts.skip !== 'color' && f.colors.size) out = out.filter((r) => r.colors.some((c) => f.colors.has(c)));
if (opts.skip !== 'style' && f.styles.size) out = out.filter((r) => r.styles.some((c) => f.styles.has(c)));
if (opts.skip !== 'material' && f.materials.size) out = out.filter((r) => r.materials.some((c) => f.materials.has(c)));
@@ -174,6 +191,7 @@ const sorters = {
title: (a, b) => str(a.title).localeCompare(str(b.title)),
vendor: (a, b) => str(a.vendor).localeCompare(str(b.vendor)) || str(a.title).localeCompare(str(b.title)),
type: (a, b) => str(a.type).localeCompare(str(b.type)) || str(a.title).localeCompare(str(b.title)),
+ series: (a, b) => str(a.series).localeCompare(str(b.series)) || str(a.sku).localeCompare(str(b.sku)),
sku: (a, b) => str(a.sku).localeCompare(str(b.sku)),
pattern: (a, b) => str(a.pattern).localeCompare(str(b.pattern)),
status: (a, b) => str(a.status).localeCompare(str(b.status)),
@@ -190,7 +208,7 @@ const sorters = {
// per-sort "row actually has this value" — used to keep empties last on dir=desc
const HAS_VALUE = {
sku: (r) => r.sku != null && r.sku !== '', title: (r) => !!r.title, vendor: (r) => !!r.vendor,
- type: (r) => !!r.type, pattern: (r) => !!r.pattern, status: (r) => !!r.status, lifecycle: (r) => !!r.lifecycle,
+ type: (r) => !!r.type, series: (r) => !!r.series, pattern: (r) => !!r.pattern, status: (r) => !!r.status, lifecycle: (r) => !!r.lifecycle,
price: (r) => r.price != null, price_asc: (r) => r.price != null, price_desc: (r) => r.price != null,
color: (r) => r.colors.length > 0, style: (r) => r.styles.length > 0, material: (r) => r.materials.length > 0,
created: (r) => r.created > 0,
@@ -262,6 +280,7 @@ const server = http.createServer((req, res) => {
res.end(JSON.stringify({
vendor: tally(applyFilters(ROWS, f, { skip: 'vendor' }), 'vendor'),
type: tally(applyFilters(ROWS, f, { skip: 'type' }), 'type'),
+ series: tally(applyFilters(ROWS, f, { skip: 'series' }), 'series'),
lifecycle: tally(applyFilters(ROWS, f, { skip: 'lifecycle' }), 'lifecycle'),
status: tally(applyFilters(ROWS, f, { skip: 'lifecycle' }), 'status'),
color: tallyArr(applyFilters(ROWS, f, { skip: 'color' }), 'colors'),
← 7f71cd1 microsites.html: density slider 1-30 with live readout + sor
·
back to All Designerwallcoverings
·
grid: Book/Series facet in left rail + Website/Internal butt a5f3545 →