[object Object]

← back to Fabricut Landing

Public site hardening (Cody red-team FIX FIRST): remove vendor-email/selection/curate routes, strip internal chrome (pill/modal/Stock-Price/net-cost/ordering-internal) via isPublic guard, fix title (no dw_unified), drop Phillipe-Romano curate button, add robots + log-only inquiry

5048cfe91c231273b8da3e5f74b39d6d5ac5b08d · 2026-08-20 10:44:10 -0700 · Steve Abrams

Files touched

Diff

commit 5048cfe91c231273b8da3e5f74b39d6d5ac5b08d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 20 10:44:10 2026 -0700

    Public site hardening (Cody red-team FIX FIRST): remove vendor-email/selection/curate routes, strip internal chrome (pill/modal/Stock-Price/net-cost/ordering-internal) via isPublic guard, fix title (no dw_unified), drop Phillipe-Romano curate button, add robots + log-only inquiry
---
 public/curate.html  | 162 ----------------------------------------------------
 public/index.html   |   7 ++-
 public/product.html |  15 ++++-
 server.js           |  50 +++++-----------
 4 files changed, 32 insertions(+), 202 deletions(-)

diff --git a/public/curate.html b/public/curate.html
deleted file mode 100644
index 7bee9ce..0000000
--- a/public/curate.html
+++ /dev/null
@@ -1,162 +0,0 @@
-<!doctype html><html lang="en"><head><meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Select · Fabricut → Phillipe Romano</title>
-<style>
-:root{--cols:8;--bg:#0c0d12;--panel:#15171f;--ink:#eef0f5;--mut:#9aa0b0;--acc:#5eead4;--line:#262a36;--new:#f59e0b}
-*{box-sizing:border-box}
-body{margin:0;background:var(--bg);color:var(--ink);font:13px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
-header{position:sticky;top:0;z-index:9;background:linear-gradient(180deg,#0c0d12,#0c0d12e8);backdrop-filter:blur(8px);
- border-bottom:1px solid var(--line);padding:10px 14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
-h1{font-size:14px;margin:0;font-weight:700}
-h1 .pl{color:var(--acc)}
-.pill{background:var(--new);color:#1a1205;font-weight:700;border-radius:999px;padding:2px 10px;font-size:12px;font-variant-numeric:tabular-nums}
-.muted{color:var(--mut)}
-select,input[type=text]{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:5px 8px;font-size:12.5px}
-input[type=range]{accent-color:var(--acc);width:150px}
-.btn{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:5px 11px;cursor:pointer;font-size:12.5px;text-decoration:none}
-.btn:hover{border-color:var(--acc)}
-.btn.on{outline:1px solid var(--acc);color:var(--acc)}
-.ctl{display:flex;align-items:center;gap:6px}
-/* texture-only grid — the image IS the tile */
-.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:6px;padding:8px 10px 70px}
-.tile{position:relative;aspect-ratio:1/1;background:#0a0b0f;border:1px solid var(--line);border-radius:2px;overflow:hidden;cursor:pointer;user-select:none}
-.tile img{width:100%;height:100%;object-fit:cover;display:block;pointer-events:none}
-.tile:hover{border-color:#414a5e}
-.tile.sel{border-color:var(--acc);box-shadow:inset 0 0 0 2px var(--acc)}
-.tile.sel::after{content:'✓';position:absolute;top:5px;right:5px;width:20px;height:20px;border-radius:50%;
- background:var(--acc);color:#04251e;font-weight:800;font-size:13px;display:flex;align-items:center;justify-content:center}
-.tile .tip{position:absolute;left:0;right:0;bottom:0;background:#000b;color:#cbd5e1;font-size:10px;padding:2px 6px;
- white-space:nowrap;overflow:hidden;text-overflow:ellipsis;opacity:0;transition:opacity .12s}
-.tile:hover .tip{opacity:1}
-#more{display:block;margin:8px auto 70px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 18px;cursor:pointer}
-#empty{padding:60px;text-align:center;color:var(--mut)}
-/* selection tray */
-#tray{position:fixed;left:0;right:0;bottom:0;z-index:20;display:flex;gap:10px;align-items:center;flex-wrap:wrap;
- background:#0e0f15f2;border-top:1px solid var(--line);padding:9px 14px;backdrop-filter:blur(8px)}
-#tray .cnt{font-weight:700;color:var(--acc);font-variant-numeric:tabular-nums}
-#save{font-size:11px;color:var(--mut)}
-</style>  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
-</head>
-<body>
-<header>
-  <h1>Fabricut → <span class="pl">Phillipe Romano</span> · Select</h1>
-  <span class="pill" id="total">…</span>
-  <span class="muted" id="meta"></span>
-  <div class="ctl"><input type="text" id="q" placeholder="search pattern / sku / color…"></div>
-  <div class="ctl"><select id="book"><option value="">All books</option></select></div>
-  <div class="ctl"><select id="color"><option value="">All colors</option></select></div>
-  <div class="ctl"><label class="muted">Grid</label>
-    <input type="range" id="density" min="1" max="20" step="1"><span class="muted" id="denv"></span></div>
-  <a class="btn" href="/">← Catalog</a>
-</header>
-<div class="grid" id="grid"></div>
-<button id="more" style="display:none">Load more</button>
-<div id="empty" style="display:none">No products match.</div>
-<div id="tray">
-  <span><span class="cnt" id="selcount">0</span> selected</span>
-  <button class="btn" id="selvis">☑ Select visible</button>
-  <button class="btn" id="unselvis">☐ Unselect visible</button>
-  <button class="btn" id="showsel">Show selected only</button>
-  <button class="btn" id="clear">✕ Clear all</button>
-  <button class="btn" id="csv">⬇ Export CSV</button>
-  <span id="save" class="muted"></span>
-  <span class="muted" style="margin-left:auto">click = select · shift-click = range · ⌘-click = open product</span>
-</div>
-<script>
-const $=s=>document.querySelector(s);
-const grid=$('#grid');
-let ALL=[], VIEW=[], shown=0, lastIdx=null, showSelOnly=false;
-const PAGE=400;
-const SEL=new Set();          // dw_sku set — the Phillipe Romano candidate list
-const BY_SKU={};              // dw_sku -> product
-function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
-const dens=localStorage.getItem('fabricut.cur.density')||'8';
-$('#density').value=dens; $('#denv').textContent=dens;
-document.documentElement.style.setProperty('--cols',dens);
-$('#density').oninput=()=>{const v=$('#density').value;document.documentElement.style.setProperty('--cols',v);
- $('#denv').textContent=v;localStorage.setItem('fabricut.cur.density',v);};
-
-function tile(p,idx){
-  const on=SEL.has(p.dw_sku);
-  return `<div class="tile${on?' sel':''}" data-sku="${esc(p.dw_sku)}" data-idx="${idx}" data-href="/product/${encodeURIComponent(p.handle)}">
-    <img loading="lazy" src="${esc(p.swatch)}" alt="${esc(p.display_name||p.sku||'')}">
-    <span class="tip">${esc(p.series||p.display_name||'')}${p.color?' · '+esc(p.color):''} · ${esc(p.sku||'')}</span>
-  </div>`;
-}
-grid.addEventListener('click',e=>{
-  const t=e.target.closest('.tile'); if(!t)return;
-  if(e.metaKey||e.ctrlKey){window.open(t.dataset.href,'_blank');return;}
-  const idx=Number(t.dataset.idx);
-  if(e.shiftKey&&lastIdx!=null){
-    const [a,b]=[Math.min(lastIdx,idx),Math.max(lastIdx,idx)];
-    const turnOn=!SEL.has(t.dataset.sku);
-    for(let i=a;i<=b;i++){const p=VIEW[i];if(!p)continue;turnOn?SEL.add(p.dw_sku):SEL.delete(p.dw_sku);}
-    repaint();
-  }else{
-    SEL.has(t.dataset.sku)?SEL.delete(t.dataset.sku):SEL.add(t.dataset.sku);
-    t.classList.toggle('sel',SEL.has(t.dataset.sku));
-  }
-  lastIdx=idx; syncCount(); saveSoon();
-});
-function repaint(){document.querySelectorAll('.tile').forEach(t=>t.classList.toggle('sel',SEL.has(t.dataset.sku)));}
-function syncCount(){$('#selcount').textContent=SEL.size.toLocaleString();}
-let st;
-function saveSoon(){clearTimeout(st);st=setTimeout(saveNow,700);}
-async function saveNow(){
-  try{
-    const r=await fetch(location.origin+'/api/selection',{method:'POST',headers:{'Content-Type':'application/json'},
-      body:JSON.stringify({skus:[...SEL]})}).then(r=>r.json());
-    $('#save').textContent=r.ok?('saved '+new Date().toLocaleTimeString()):'save failed';
-  }catch{$('#save').textContent='save failed';}
-}
-function applyFilters(){
-  const bk=$('#book').value, cl=$('#color').value, q=$('#q').value.trim().toLowerCase();
-  VIEW=ALL.filter(p=>
-    (!showSelOnly||SEL.has(p.dw_sku))&&
-    (!bk||p.book===bk)&&(!cl||p.color_bucket===cl)&&
-    (!q||[p.display_name,p.series,p.sku,p.dw_sku,p.color,p.book].some(v=>v&&String(v).toLowerCase().includes(q))));
-  shown=0; lastIdx=null; grid.innerHTML=''; renderMore();
-  $('#total').textContent=VIEW.length.toLocaleString()+' designs';
-  $('#empty').style.display=VIEW.length?'none':'block';
-}
-function renderMore(){
-  const slice=VIEW.slice(shown,shown+PAGE);
-  grid.insertAdjacentHTML('beforeend',slice.map((p,i)=>tile(p,shown+i)).join(''));
-  shown+=slice.length;
-  $('#meta').textContent=`${shown.toLocaleString()} of ${VIEW.length.toLocaleString()} shown`;
-  $('#more').style.display=shown<VIEW.length?'block':'none';
-}
-$('#more').onclick=renderMore;
-addEventListener('scroll',()=>{if(shown<VIEW.length&&innerHeight+scrollY>=document.body.offsetHeight-600)renderMore();});
-$('#book').onchange=applyFilters; $('#color').onchange=applyFilters;
-let t; $('#q').oninput=()=>{clearTimeout(t);t=setTimeout(applyFilters,300);};
-$('#selvis').onclick=()=>{VIEW.slice(0,shown).forEach(p=>SEL.add(p.dw_sku));repaint();syncCount();saveSoon();};
-$('#unselvis').onclick=()=>{VIEW.slice(0,shown).forEach(p=>SEL.delete(p.dw_sku));repaint();syncCount();saveSoon();};
-$('#showsel').onclick=()=>{showSelOnly=!showSelOnly;$('#showsel').classList.toggle('on',showSelOnly);applyFilters();};
-$('#clear').onclick=()=>{if(!SEL.size||confirm('Clear all '+SEL.size+' selected?')){SEL.clear();repaint();syncCount();saveSoon();applyFilters();}};
-$('#csv').onclick=()=>{
-  const rows=[['dw_sku','vendor_sku','pattern','color','book','handle','swatch_url']];
-  for(const s of SEL){const p=BY_SKU[s];if(!p)continue;
-    rows.push([p.dw_sku,p.sku,p.series||p.display_name,p.color||'',p.book||'',p.handle,p.swatch||'']);}
-  const csv=rows.map(r=>r.map(v=>'"'+String(v??'').replace(/"/g,'""')+'"').join(',')).join('\n');
-  const a=document.createElement('a');
-  a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'}));
-  a.download='phillipe-romano-candidates-'+new Date().toISOString().slice(0,10)+'.csv';
-  a.click();
-};
-async function load(){
-  const [pd,fc,sel]=await Promise.all([
-    fetch(location.origin+'/api/products').then(r=>r.json()),
-    fetch(location.origin+'/api/facets').then(r=>r.json()),
-    fetch(location.origin+'/api/selection').then(r=>r.json())]);
-  ALL=pd.products; ALL.forEach(p=>BY_SKU[p.dw_sku]=p);
-  (sel.skus||[]).forEach(s=>SEL.add(s));
-  $('#book').innerHTML='<option value="">All books</option>'+(fc.books||[]).map(([b,n])=>`<option value="${esc(b)}">${esc(b)} (${n.toLocaleString()})</option>`).join('');
-  const colors={}; ALL.forEach(p=>{if(p.color_bucket)colors[p.color_bucket]=(colors[p.color_bucket]||0)+1;});
-  $('#color').innerHTML='<option value="">All colors</option>'+Object.entries(colors).sort((a,b)=>b[1]-a[1]).map(([c,n])=>`<option value="${esc(c)}">${esc(c)} (${n.toLocaleString()})</option>`).join('');
-  syncCount(); applyFilters();
-}
-load();
-</script>
-  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
-</body></html>
diff --git a/public/index.html b/public/index.html
index 5858e0b..c6589d1 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,6 +1,6 @@
 <!doctype html><html lang="en"><head><meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Fabricut · Catalog · dw_unified</title>
+<title>Fabricut — A Designer Wallcoverings Collection</title>
 <style>
 :root{--cols:6;--fs:1;--bg:#0c0d12;--panel:#15171f;--card:#12141b;--ink:#eef0f5;--mut:#9aa0b0;--acc:#5eead4;--line:#262a36;
  --new:#f59e0b;--pub:#34d399;--unpub:#94a3b8;--arch:#f87171;--ship:#60a5fa}
@@ -151,6 +151,7 @@ body.listmode #listhead{display:grid}
 .fabricut-refresh-pill.rp-live .rp-dot{background:var(--new);animation:rppulse 1s ease-in-out infinite}
 @keyframes rppulse{0%,100%{opacity:1}50%{opacity:.25}}
 </style>  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+<style>body[data-public] .internal-pill,body[data-public] .modal-back,body[data-public] .act3,body[data-public] [data-act="stock"],body[data-public] [data-act="price"],body[data-public] .reqact{display:none!important}</style>
 </head>
 <body>
 <header>
@@ -171,7 +172,6 @@ body.listmode #listhead{display:grid}
   <div class="ctl"><label class="muted">Density</label>
     <input type="range" id="density" min="3" max="20" step="1"></div>
   <button class="btn" id="view">▤ List view</button>
-  <a class="btn" href="/curate" title="bulk-select candidates for the Phillipe Romano private label">✓ Select for PR</a>
 </header>
 <div id="layout">
 <aside aria-label="filters">
@@ -442,6 +442,7 @@ $('#view').onclick=()=>{localStorage.setItem('fabricut.view',localStorage.getIte
 applyView();
 addEventListener('scroll',()=>{if(shown<VIEW.length&&innerHeight+scrollY>=document.body.offsetHeight-600)renderMore();});
 async function loadAll(){
+  try{const cfg=await fetch(location.origin+"/api/config").then(r=>r.json());if(cfg.isPublic)document.body.dataset.public="1";}catch(e){document.body.dataset.public="1";}
   const [pd,fc]=await Promise.all([
     fetch(location.origin+'/api/products').then(r=>r.json()),
     fetch(location.origin+'/api/facets').then(r=>r.json())]);
@@ -453,7 +454,7 @@ loadAll();
 addEventListener('popstate',()=>{readURL();applyFilters('none');});
 
 /* ── Corner refresh countdown ──────────────────────────────────────────────
-   Data is rebuilt from dw_unified every 15 min by a cron. This pill counts down
+   Data refreshes every 15 minutes. This pill counts down
    to the next refresh (synced to the real data-file timestamp via /api/meta) and
    pulls fresh data in-place when it lands — no manual reload. */
 (function(){
diff --git a/public/product.html b/public/product.html
index a5b7588..79156f2 100644
--- a/public/product.html
+++ b/public/product.html
@@ -3,12 +3,13 @@
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width,initial-scale=1">
-<title>Designer Wallcoverings</title>
+<title>Fabricut — A Designer Wallcoverings Collection</title>
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 <link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
 <link rel="stylesheet" href="/styles.css">
   <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+<style>body[data-public] .internal-pill,body[data-public] .modal-back,body[data-public] .act3,body[data-public] [data-act="stock"],body[data-public] [data-act="price"],body[data-public] .reqact{display:none!important}</style>
 </head>
 <body data-nav-agent="off"><!-- nav-agent off (TK-10525): product DETAIL page (single product + 6-item pairs strip), no browsable grid to control -->
 
@@ -51,6 +52,7 @@ function specRow(k,v){return v?`<div class="row"><span class="k">${k}</span><spa
 function specDrill(k,field,v){return v?`<div class="row"><span class="k">${k}</span><span class="v"><a class="drill" href="/?${field}=${encodeURIComponent(v)}" title="See all with ${esc(k)} = ${esc(v)}">${esc(v)}</a></span></div>`:'';}
 
 let VM={}; // vendorMeta from /api/config — phone, account #, discount, pricing model
+let IS_PUBLIC=false; // set from cfg.isPublic — hides all internal (net/cost/ordering/request) UI
 const money=n=>n==null?null:'$'+Number(n).toFixed(2);
 
 // Internal pricing — retail / designer net / our cost, per the line's pricing model.
@@ -74,6 +76,12 @@ function priceBox(p){
   const unit=VM.pricing_unit?('/'+String(VM.pricing_unit).replace(/^per\s*/i,'').replace('yard','yd').replace('roll','roll')):'';
   const row=(k,v)=>`<div class="row"><span class="k">${k}</span><span class="v">${v!=null?esc(money(v)+unit):'—'}</span></div>`;
   const empty=t.retail==null&&t.net==null&&t.ourCost==null;
+  // PUBLIC: show only the per-yard Retail (customer price). INTERNAL adds Designer Net + Our Cost.
+  if(IS_PUBLIC){
+    return `<div class="pricebox">
+      ${t.retail!=null?row('Price',t.retail):'<div class="pnote">Contact us for pricing.</div>'}
+    </div>`;
+  }
   return `<div class="pricebox">
     ${row('Retail',t.retail)}
     ${row('Designer Net',t.net)}
@@ -87,6 +95,7 @@ function priceBox(p){
   const [cfg,r]=await Promise.all([fetch(location.origin+'/api/config').then(x=>x.json()).catch(()=>({})), fetch(location.origin+'/api/product/'+encodeURIComponent(handle))]);
   const LINE = cfg.line || cfg.vendor || 'Fabricut';
   VM = cfg.vendorMeta || {};
+  IS_PUBLIC = !!cfg.isPublic; if(IS_PUBLIC) document.body.dataset.public='1';
   if(cfg.slug){ document.body.dataset.line = cfg.slug; }
   document.querySelector('.corner.ul').setAttribute('href', cfg.houseUrl || 'https://www.designerwallcoverings.com');
   if(!r.ok){document.getElementById('pdp').innerHTML='<div class="crumbs"><a href="/">← Back</a></div><h1 style="font-family:var(--serif)">Not found</h1>';return;}
@@ -136,7 +145,9 @@ function priceBox(p){
     <div class="story">
       <div><h3>The story</h3><div>${p.body_html?('<p>'+esc(p.body_html.slice(0,700))+(p.body_html.length>700?'…':'')+'</p>'):('<p>'+esc(p.title)+' from the '+esc(LINE)+' collection.</p>')}</div></div>
       <div><h3>Specifications</h3><p>${esc([p.width,p.repeat&&('repeat '+p.repeat),p.material].filter(Boolean).join(' · ')||'Digitally printed, made to order.')}</p></div>
-      <div><h3>Ordering (internal)</h3><p>Order direct from ${esc(VM.name||LINE)}${VM.phone?' at '+esc(VM.phone):''}${VM.account_number?', our account # '+esc(VM.account_number):''}. Use the buttons above to log a memo, stock, or price request for purchasing.</p></div>
+      ${IS_PUBLIC
+        ? `<div><h3>How to order</h3><p>${esc(LINE)} wallcoverings are sold by the yard through Designer Wallcoverings. <a href="https://www.designerwallcoverings.com" target="_blank" rel="noopener noreferrer">shop the collection at Designer Wallcoverings</a>.</p></div>`
+        : `<div><h3>Ordering (internal)</h3><p>Order direct from ${esc(VM.name||LINE)}${VM.phone?' at '+esc(VM.phone):''}${VM.account_number?', our account # '+esc(VM.account_number):''}. Use the buttons above to log a memo, stock, or price request for purchasing.</p></div>`}
     </div>
     <section class="pairs" id="pairs" hidden>
       <h3>Pairs well with</h3>
diff --git a/server.js b/server.js
index aa38774..a10ce39 100644
--- a/server.js
+++ b/server.js
@@ -38,7 +38,7 @@ fs.watchFile(DATA, { interval: 5000 }, (cur, prev) => {
   if (cur.mtimeMs !== prev.mtimeMs) { try { load(); } catch (e) { console.error('[fabricut] reload failed:', e.message); } }
 });
 
-// House identity — Designer Wallcoverings is the brand; Astek is the featured line.
+// House identity — Designer Wallcoverings is the brand; Fabricut is the featured line.
 const CONFIG = {
   house: 'Designer Wallcoverings',
   houseUrl: 'https://www.designerwallcoverings.com',
@@ -56,6 +56,7 @@ const CONFIG = {
   title: 'Fabricut — A Designer Wallcoverings Collection',
   metaDescription: 'The Fabricut wallcovering collection at Designer Wallcoverings. To the trade — order a memo sample before specifying.',
   slug: 'fabricut',
+  isPublic: true,
   palette: null,
   about: {
     paragraphs: [
@@ -102,48 +103,27 @@ app.get('/api/pairs/:handle', (req, res) => {
   res.json({ pairs: out });
 });
 
-// Internal purchasing requests — Request Memo / Check Stock / Get Price.
-// REQ-numbered in dw_unified.vendor_requests; stock/price auto-email the vendor
-// via george-gmail when an email is on file (account # only, never a client name).
-const { mountVendorRequests } = require('./lib/vendor-requests');
-mountVendorRequests(app, { vendorCode: 'fabricut', getVendor: () => SNAP.vendor || {}, dataDir: path.join(__dirname, 'data') });
-
-// Legacy inquiry endpoint — kept for anything still posting here; logs only.
-const ACTION_MSG = {
-  memo: 'Memo request logged — purchasing will order the sample.',
-  stock: 'Stock check logged — purchasing will confirm availability with the vendor.',
-  price: 'Price request logged — purchasing will confirm current pricing with the vendor.',
-};
+// PUBLIC site — NO vendor-ops routes. The internal twin owns Memo/Stock/Price
+// (which auto-email the vendor with our account #) and the private-label /curate
+// + /api/selection. Cody red-team 2026-08-20: those must NOT exist on the public
+// domain (unauthenticated vendor-email trigger + private-label disclosure).
+//
+// The ONLY public write is a sample-REQUEST that logs to a local file — it NEVER
+// emails the vendor, never exposes our account #, and is always type=memo.
 app.post('/api/inquiry', (req, res) => {
   const { sku, name, email, note } = req.body || {};
-  const type = ['memo', 'stock', 'price'].includes(req.body && req.body.type) ? req.body.type : 'memo';
   if (!sku || !email) return res.status(400).json({ ok: false, error: 'sku and email required' });
-  const rec = { at: new Date().toISOString(), type, sku, name: name || '', email, note: note || '', ip: req.ip };
-  try {
-    fs.appendFileSync(path.join(__dirname, 'data', 'inquiries.jsonl'), JSON.stringify(rec) + '\n');
-  } catch (e) { return res.status(500).json({ ok: false, error: 'log failed' }); }
-  res.json({ ok: true, message: ACTION_MSG[type] });
+  const rec = { at: new Date().toISOString(), type: 'memo', sku, name: name || '', email, note: note || '', ip: req.ip };
+  try { fs.appendFileSync(path.join(__dirname, 'data', 'inquiries.jsonl'), JSON.stringify(rec) + '\n'); }
+  catch (e) { return res.status(500).json({ ok: false, error: 'log failed' }); }
+  res.json({ ok: true, message: 'Sample request received — we will be in touch.' });
 });
 
-// ── Private-label curation (Fabricut curation) ─────────────────────
-// Selection is the FIRST step only — picks persist server-side so the later
-// (Steve-gated) private-label push has a durable list. NO Shopify writes here.
-const SEL = path.join(__dirname, 'data', 'selection.json');
-app.get('/api/selection', (_req, res) => {
-  try { res.json(JSON.parse(fs.readFileSync(SEL, 'utf8'))); }
-  catch { res.json({ updated_at: null, count: 0, skus: [] }); }
-});
-app.post('/api/selection', (req, res) => {
-  const skus = Array.isArray(req.body?.skus) ? req.body.skus.filter(s => typeof s === 'string').slice(0, 20000) : null;
-  if (!skus) return res.status(400).json({ ok: false, error: 'skus[] required' });
-  const doc = { updated_at: new Date().toISOString(), label: 'Fabricut curation candidates', count: skus.length, skus };
-  fs.writeFileSync(SEL, JSON.stringify(doc, null, 1));
-  res.json({ ok: true, count: skus.length });
-});
+// robots — public site indexes; internal-only routes/pages don't exist here.
+app.get('/robots.txt', (_req, res) => res.type('text/plain').send('User-agent: *\nAllow: /\nSitemap: https://fabricut.designerwallcoverings.com/sitemap.xml\n'));
 
 app.use(express.static(path.join(__dirname, 'public')));
 app.get('/product/:handle', (_req, res) => res.sendFile(path.join(__dirname, 'public', 'product.html')));
-app.get('/curate', (_req, res) => res.sendFile(path.join(__dirname, 'public', 'curate.html')));
 
 const server = app.listen(PORT, () => {
   console.log(`Fabricut landing → http://127.0.0.1:${server.address().port}`);

← 7c24d19 Fabricut microsite (fabricut-landing): DW-branded self-conta  ·  back to Fabricut Landing  ·  SEO: dynamic sitemap on public + noindex robots on internal 935a208 →