← back to Dw Collections Viewer
save+deploy: sort selector (textures/newest/title/sku/priceUp/priceDown/random) wired through API → renderSite; per CLAUDE.md grid sort+density rule
71f733796d067fdb9759bc41af063ba824fabadd · 2026-05-06 17:42:44 -0700 · Steve
Files touched
A data/site-status.jsonM public/index.htmlM server.js
Diff
commit 71f733796d067fdb9759bc41af063ba824fabadd
Author: Steve <steve@designerwallcoverings.com>
Date: Wed May 6 17:42:44 2026 -0700
save+deploy: sort selector (textures/newest/title/sku/priceUp/priceDown/random) wired through API → renderSite; per CLAUDE.md grid sort+density rule
---
data/site-status.json | 232 ++++++++++++++++++++++++++++++++++++++++++++++++++
public/index.html | 39 +++++++--
server.js | 30 +++++--
3 files changed, 285 insertions(+), 16 deletions(-)
diff --git a/data/site-status.json b/data/site-status.json
new file mode 100644
index 0000000..3cc2e1e
--- /dev/null
+++ b/data/site-status.json
@@ -0,0 +1,232 @@
+{
+ "silkwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:14.733Z"
+ },
+ "linenwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:14.734Z"
+ },
+ "raffiawallcoverings": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:14.889Z"
+ },
+ "corkwallcovering": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:14.903Z"
+ },
+ "jutewallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:14.913Z"
+ },
+ "silkwallcoverings": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:14.940Z"
+ },
+ "textilewallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:14.980Z"
+ },
+ "fabricwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:14.999Z"
+ },
+ "raffiawalls": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.004Z"
+ },
+ "silverleafwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.008Z"
+ },
+ "vinylwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.129Z"
+ },
+ "metallicwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.147Z"
+ },
+ "micawallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.195Z"
+ },
+ "mylarwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.262Z"
+ },
+ "suedewallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.314Z"
+ },
+ "madagascarwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.321Z"
+ },
+ "stringwallpaper": {
+ "http_status": 301,
+ "redirect_target": "linenwallpaper.com",
+ "probed_at": "2026-05-07T00:33:15.432Z"
+ },
+ "embroideredwallpaper": {
+ "http_status": 301,
+ "redirect_target": "fabricwallpaper.com",
+ "probed_at": "2026-05-07T00:33:15.438Z"
+ },
+ "1800swallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.528Z"
+ },
+ "1890swallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.533Z"
+ },
+ "1900swallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.561Z"
+ },
+ "1930swallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.591Z"
+ },
+ "1920swallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.637Z"
+ },
+ "1940swallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.640Z"
+ },
+ "1960swallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.659Z"
+ },
+ "1970swallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.715Z"
+ },
+ "1950swallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.724Z"
+ },
+ "1980swallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.839Z"
+ },
+ "wallpapersback": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.840Z"
+ },
+ "retrowalls": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.861Z"
+ },
+ "agedwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.880Z"
+ },
+ "museumwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.938Z"
+ },
+ "handcraftedwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.960Z"
+ },
+ "pastelwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:15.996Z"
+ },
+ "restorationwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:16.076Z"
+ },
+ "glitterwalls": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:16.155Z"
+ },
+ "greenwallcoverings": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:16.232Z"
+ },
+ "contractwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:16.318Z"
+ },
+ "naturalwallcoverings": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:16.319Z"
+ },
+ "recycledwallpaper": {
+ "http_status": 405,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:16.361Z"
+ },
+ "hospitalitywallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:16.438Z"
+ },
+ "hotelwallcoverings": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:16.473Z"
+ },
+ "healthcarewallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:16.476Z"
+ },
+ "restaurantwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:16.483Z"
+ },
+ "saloonwallpaper": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:16.541Z"
+ },
+ "architecturalwallcoverings": {
+ "http_status": 200,
+ "redirect_target": null,
+ "probed_at": "2026-05-07T00:33:16.601Z"
+ }
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 4d584df..702ccda 100644
--- a/public/index.html
+++ b/public/index.html
@@ -763,10 +763,26 @@ function renderDetail() {
<div class="actions">
<button class="btn" onclick="saveColls()">Save collections</button>
+ <label class="deploy-sort" style="display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.06em;color:#999;text-transform:uppercase">
+ Sort:
+ <select id="deploySortSel" onchange="localStorage.setItem('dwc_deploy_sort', this.value)" style="background:#0e0e10;color:#f4f1ea;border:1px solid #2a2a32;padding:5px 8px;font-family:inherit;font-size:11px">
+ <option value="textures">textures first (default)</option>
+ <option value="newest">newest</option>
+ <option value="title">title A→Z</option>
+ <option value="sku">SKU A→Z</option>
+ <option value="priceUp">price ↑</option>
+ <option value="priceDown">price ↓</option>
+ <option value="random">random</option>
+ </select>
+ </label>
<button class="btn go" onclick="saveAndDeploy()" ${assigned.length === 0 ? 'disabled' : ''}>💾 Save & Deploy</button>
<button class="btn" onclick="renderSite()" ${assigned.length === 0 ? 'disabled' : ''}>Render now (live)</button>
<a class="btn" href="https://${s.domain}" target="_blank" style="text-decoration:none">Open site ↗</a>
</div>
+ <script>
+ // Restore saved deploy sort
+ (function(){ const v = localStorage.getItem('dwc_deploy_sort'); if (v) { const sel = document.getElementById('deploySortSel'); if (sel) sel.value = v; } })();
+ </script>
`;
// Hydrate the products slider after the detail HTML is in the DOM.
renderProductsSlider();
@@ -856,16 +872,22 @@ async function saveColls() {
toast(`saved · ${j.collections.length} collections assigned to ${CURRENT}`);
}
+function getDeploySort() {
+ const sel = document.getElementById('deploySortSel');
+ return (sel && sel.value) || localStorage.getItem('dwc_deploy_sort') || 'textures';
+}
+
async function renderSite() {
- if (!confirm(`Re-render ${CURRENT} from ${SITES[CURRENT].collections.length} Shopify collection(s)? This will overwrite the current products.json + scp + pm2 restart.`)) return;
- toast(`rendering ${CURRENT}…`);
+ const sort = getDeploySort();
+ if (!confirm(`Re-render ${CURRENT} from ${SITES[CURRENT].collections.length} Shopify collection(s), sorted "${sort}"? Overwrites products.json + scp + pm2 restart.`)) return;
+ toast(`rendering ${CURRENT} (sort=${sort})…`);
const r = await fetch(`api/site/${CURRENT}/render`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ deploy: true })
+ body: JSON.stringify({ deploy: true, sort })
});
const j = await r.json();
if (!r.ok) return toast('render failed: ' + (j.error || r.status), false);
- toast(`${CURRENT} rendered · ${j.total_unique} unique products · deployed: ${j.deployed}`);
+ toast(`${CURRENT} rendered · ${j.total_unique} products · sort=${sort} · deployed: ${j.deployed}`);
await loadSites();
renderDetail();
}
@@ -875,7 +897,8 @@ async function saveAndDeploy() {
const slug = CURRENT;
const colls = SITES[slug].collections || [];
if (!colls.length) return toast('no collections assigned', false);
- if (!confirm(`Save & deploy ${slug}? Persists ${colls.length} collection(s), re-renders products.json from Shopify, and pm2-restarts the live site.`)) return;
+ const sort = getDeploySort();
+ if (!confirm(`Save & deploy ${slug} (sort: ${sort})? Persists ${colls.length} collection(s), re-renders products.json from Shopify in "${sort}" order, scp + pm2 restart.`)) return;
toast(`saving ${slug}…`);
const r1 = await fetch(`api/site/${slug}/collections`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -883,14 +906,14 @@ async function saveAndDeploy() {
});
const j1 = await r1.json();
if (!r1.ok) return toast('save failed: ' + (j1.error || r1.status), false);
- toast(`saved · deploying ${slug}…`);
+ toast(`saved · deploying ${slug} (sort=${sort})…`);
const r2 = await fetch(`api/site/${slug}/render`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ deploy: true }),
+ body: JSON.stringify({ deploy: true, sort }),
});
const j2 = await r2.json();
if (!r2.ok) return toast('deploy failed: ' + (j2.error || r2.status), false);
- toast(`${slug} live · ${j2.total_unique} products · deployed: ${j2.deployed}`);
+ toast(`${slug} live · ${j2.total_unique} products · sort=${sort} · deployed: ${j2.deployed}`);
await loadSites();
renderDetail();
}
diff --git a/server.js b/server.js
index 2a768eb..7420cc8 100644
--- a/server.js
+++ b/server.js
@@ -272,12 +272,22 @@ async function renderSite(slug, opts = {}) {
}
}
- // Sort textures-first → patterns. If hero_sku is pinned, bubble it to position 0.
- const arr = Array.from(merged.values()).sort((a, b) => {
- const sa = texScore(a), sb = texScore(b);
- if (sa !== sb) return sb - sa;
- return (a.title || '').localeCompare(b.title || '');
- }).slice(0, 600);
+ // Sort: caller picks via opts.sort. Default = textures-first → patterns (legacy behavior).
+ // Available: 'textures' (default), 'newest', 'sku', 'title', 'priceUp', 'priceDown', 'random'.
+ const all = Array.from(merged.values());
+ const sortMode = String(opts.sort || rule.sort || 'textures').toLowerCase();
+ const sorters = {
+ textures: (a, b) => (texScore(b) - texScore(a)) || (a.title || '').localeCompare(b.title || ''),
+ newest: (a, b) => String(b.published_at || b.created_at || '').localeCompare(String(a.published_at || a.created_at || '')),
+ sku: (a, b) => String(a.sku || a.handle || '').localeCompare(String(b.sku || b.handle || '')),
+ title: (a, b) => String(a.title || '').localeCompare(String(b.title || '')),
+ priceUp: (a, b) => (a.max_price || 0) - (b.max_price || 0),
+ priceDown: (a, b) => (b.max_price || 0) - (a.max_price || 0),
+ random: () => Math.random() - 0.5,
+ };
+ const cmp = sorters[sortMode] || sorters.textures;
+ const arr = all.sort(cmp).slice(0, 600);
+ console.log(`[render] ${slug} sort=${sortMode} → ${arr.length} products`);
if (rule.hero_sku) {
const i = arr.findIndex(p => p.sku === rule.hero_sku);
if (i > 0) { const [pin] = arr.splice(i, 1); arr.unshift(pin); }
@@ -440,12 +450,13 @@ app.post('/api/auto-seed', async (req, res) => {
app.post('/api/render-bulk', async (req, res) => {
const slugs = Array.isArray(req.body.slugs) ? req.body.slugs.filter(validSlug).slice(0, 50) : [];
const deploy = !!req.body.deploy;
+ const sort = String(req.body.sort || '').toLowerCase() || undefined;
if (!slugs.length) return res.status(400).json({ error: 'no slugs' });
const results = [];
for (let i = 0; i < slugs.length; i++) {
const s = slugs[i];
try {
- const out = await renderSite(s, { deploy });
+ const out = await renderSite(s, { deploy, sort });
results.push({ slug: s, ok: true, total_unique: out.total_unique, deployed: out.deployed });
} catch (e) {
results.push({ slug: s, ok: false, error: e.message });
@@ -468,7 +479,10 @@ app.post('/api/site/:slug/collections', (req, res) => {
app.post('/api/site/:slug/render', async (req, res) => {
try {
- const out = await renderSite(req.params.slug, { deploy: !!req.body.deploy });
+ const out = await renderSite(req.params.slug, {
+ deploy: !!req.body.deploy,
+ sort: String(req.body.sort || '').toLowerCase() || undefined,
+ });
res.json({ ok: true, ...out });
} catch (e) { res.status(500).json({ error: e.message }); }
});
← aa01c45 Probe live HTTP status; flag redirected/parked sites in the
·
back to Dw Collections Viewer
·
Hero resolution probe + HD/MED/SD badges + upscale to larges dd68026 →