← back to Dw Fleet Registry
gap-fill: 13 niche-starved microsites get a unique high-res hero (any-unused pool fallback, fleet-deduped) — closes the hero-dup gap to 61/62 (thesetdecorator excluded by design)
a8abc42ae95204043972b9652f97dc1c3326bdc0 · 2026-06-01 13:53:34 -0700 · SteveStudio2
Files touched
A fill-gap-report.jsonA fill-gap.mjsM hero-ledger.json
Diff
commit a8abc42ae95204043972b9652f97dc1c3326bdc0
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Mon Jun 1 13:53:34 2026 -0700
gap-fill: 13 niche-starved microsites get a unique high-res hero (any-unused pool fallback, fleet-deduped) — closes the hero-dup gap to 61/62 (thesetdecorator excluded by design)
---
fill-gap-report.json | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++
fill-gap.mjs | 120 ++++++++++++++++++++++++++++++++++++++
hero-ledger.json | 126 ++++++++++++++++++++++++++++++++++++++-
3 files changed, 407 insertions(+), 1 deletion(-)
diff --git a/fill-gap-report.json b/fill-gap-report.json
new file mode 100644
index 0000000..75b87d9
--- /dev/null
+++ b/fill-gap-report.json
@@ -0,0 +1,162 @@
+{
+ "ranAt": "2026-06-01T20:53:00.044Z",
+ "results": [
+ {
+ "slug": "embroideredwallpaper",
+ "steps": [
+ "header-component",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-any"
+ ],
+ "hero": "2400x2400",
+ "heroTier": "any",
+ "commit": "1721db5",
+ "status": "DONE"
+ },
+ {
+ "slug": "handcraftedwallpaper",
+ "steps": [
+ "header-component",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-any"
+ ],
+ "hero": "2400x2400",
+ "heroTier": "any",
+ "commit": "9d84f5e",
+ "status": "DONE"
+ },
+ {
+ "slug": "museumwallpaper",
+ "steps": [
+ "header-component",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-niche"
+ ],
+ "hero": "1600x1600",
+ "heroTier": "niche",
+ "commit": "7e6d7c4",
+ "status": "DONE"
+ },
+ {
+ "slug": "recycledwallpaper",
+ "steps": [
+ "header-component",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-any"
+ ],
+ "hero": "2400x2400",
+ "heroTier": "any",
+ "commit": "42bfc9b",
+ "status": "DONE"
+ },
+ {
+ "slug": "stringwallpaper",
+ "steps": [
+ "header-component",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-any"
+ ],
+ "hero": "1500x1500",
+ "heroTier": "any",
+ "commit": "5d33101",
+ "status": "DONE"
+ },
+ {
+ "slug": "textiletuesday",
+ "steps": [
+ "header-component",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-any"
+ ],
+ "hero": "2400x1806",
+ "heroTier": "any",
+ "commit": "0e48d70",
+ "status": "DONE"
+ },
+ {
+ "slug": "wallpaperdefinitions",
+ "steps": [
+ "header-component",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-any"
+ ],
+ "hero": "2400x2400",
+ "heroTier": "any",
+ "commit": "d57310d",
+ "status": "DONE"
+ },
+ {
+ "slug": "wallpaperdictionary",
+ "steps": [
+ "header-component",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-any"
+ ],
+ "hero": "2022x2034",
+ "heroTier": "any",
+ "commit": "aa70894",
+ "status": "DONE"
+ },
+ {
+ "slug": "wallpaperdistributors",
+ "steps": [
+ "header-component",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-any"
+ ],
+ "hero": "2000x2000",
+ "heroTier": "any",
+ "commit": "c6cd798",
+ "status": "DONE"
+ },
+ {
+ "slug": "wallpaperestimator",
+ "steps": [
+ "header-component",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-any"
+ ],
+ "hero": "2400x3600",
+ "heroTier": "any",
+ "commit": "f293ea6",
+ "status": "DONE"
+ },
+ {
+ "slug": "wallpaperglossary",
+ "steps": [
+ "header-component",
+ "header-injected",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-any"
+ ],
+ "hero": "2400x2400",
+ "heroTier": "any",
+ "commit": "46e7bde",
+ "status": "DONE"
+ },
+ {
+ "slug": "wallpaperhistory",
+ "steps": [
+ "header-component",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-any"
+ ],
+ "hero": "2034x2803",
+ "heroTier": "any",
+ "commit": "149a9f8",
+ "status": "DONE"
+ },
+ {
+ "slug": "wallpaperwednesday",
+ "steps": [
+ "header-component",
+ "accent-skip (mono/neutral or no drift)",
+ "hero-any"
+ ],
+ "hero": "2400x2400",
+ "heroTier": "any",
+ "commit": "5e811fa",
+ "status": "DONE"
+ }
+ ]
+}
diff --git a/fill-gap.mjs b/fill-gap.mjs
new file mode 100644
index 0000000..b0fab9d
--- /dev/null
+++ b/fill-gap.mjs
@@ -0,0 +1,120 @@
+#!/usr/bin/env node
+// Gap-filler for the fanout's niche-starved SKIPs (Steve 2026-06-01: "close the
+// 23-site gap first, then deploy"). The main fanout.mjs only assigns a hero when
+// the catalog pool has a NICHE-MATCHING high-res image; ~19 microsites have niches
+// the pool can't satisfy (cork/silk/suede/string/embroidered + meta/calendar sites).
+// This script gives each a UNIQUE high-res hero anyway: prefer a niche match, else
+// fall back to ANY unused pool image — still deduped fleet-wide against hero-ledger.json.
+// Same treatment as fanout (dw-header.js + corner-nav-off + accent + hero-bg.jpg) on
+// the same style-rebuild-pilot branch. LOCAL ONLY. Ledger writes re-read from disk
+// per-site so a concurrent fanout can't be clobbered.
+//
+// node fill-gap.mjs # default = the 19 known niche-starved microsites
+// node fill-gap.mjs slugA slugB # explicit list
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { execSync } from 'node:child_process';
+
+const PROJECTS = path.join(os.homedir(), 'Projects');
+const SELF = path.dirname(new URL(import.meta.url).pathname);
+const COMPONENT = path.join(SELF, 'component', 'dw-header.js');
+const LEDGER_PATH = path.join(SELF, 'hero-ledger.json');
+const audit = JSON.parse(fs.readFileSync(path.join(SELF, 'template-audit.json'), 'utf8'));
+const auditBySlug = Object.fromEntries(audit.sites.map(s => [s.slug, s]));
+const CATALOG = JSON.parse(fs.readFileSync(path.join(SELF, 'catalog-hires.json'), 'utf8')).pool;
+
+const sh = (cmd, cwd) => execSync(cmd, { cwd, stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
+const titleCase = s => s.replace(/(wallcoverings?|wallpapers?|walls?|fabric|covering|paper|cover)/gi, ' $1').replace(/\s+/g,' ').trim().replace(/\b\w/g, c => c.toUpperCase());
+const readLedger = () => fs.existsSync(LEDGER_PATH) ? JSON.parse(fs.readFileSync(LEDGER_PATH, 'utf8')) : { usedUrls: {}, sites: {} };
+function probeDims(file) {
+ try { const o = execSync(`sips -g pixelWidth -g pixelHeight "${file}" 2>/dev/null`).toString();
+ return { w:+(o.match(/pixelWidth: (\d+)/)||[])[1]||0, h:+(o.match(/pixelHeight: (\d+)/)||[])[1]||0 }; }
+ catch { return { w:0, h:0 }; }
+}
+function nicheKeywords(slug) {
+ try { const sv = fs.readFileSync(path.join(PROJECTS, slug, 'server.js'), 'utf8');
+ const m = sv.match(/NICHE_POS\s*=\s*\[([^\]]*)\]/);
+ if (m) { const ks = m[1].split(',').map(x => x.replace(/["'\s]/g, '')).filter(Boolean); if (ks.length) return ks; } } catch {}
+ return slug.toLowerCase().replace(/(wallcoverings?|wallpapers?|walls?|covering)/g, ' ').trim().split(/\s+/).filter(w => w.length > 2);
+}
+// Prefer a niche match; otherwise ANY unused pool image. Deterministic by slug so it's stable.
+function pickHero(slug, used) {
+ const niche = nicheKeywords(slug).map(k => k.toLowerCase());
+ const free = CATALOG.filter(im => !used[im.src]);
+ const pickFrom = arr => { const idx = [...slug].reduce((a,c)=>a+c.charCodeAt(0),0) % arr.length; const im = arr[idx]; return { url: im.src, base: im.src.split('?')[0], tier: arr===nicheCand?'niche':'any' }; };
+ const nicheCand = free.filter(im => niche.some(k => k && im.meta.includes(k)));
+ if (nicheCand.length) return pickFrom(nicheCand);
+ if (free.length) return pickFrom(free);
+ return null;
+}
+
+const DEFAULT = ['1900swallpaper','blockprintedwallpaper','corkwallcovering','embroideredwallpaper','handcraftedwallpaper','museumwallpaper','recycledwallpaper','screenprintedwallpaper','silkwallpaper','stringwallpaper','suedewallpaper','textiletuesday','wallpaperdefinitions','wallpaperdictionary','wallpaperdistributors','wallpaperestimator','wallpaperglossary','wallpaperhistory','wallpaperwednesday'];
+const targets = process.argv.slice(2).length ? process.argv.slice(2) : DEFAULT;
+
+const results = [];
+for (const slug of targets) {
+ const r = { slug, steps: [] };
+ try {
+ let ledger = readLedger();
+ if (ledger.sites[slug]) { r.status = 'SKIP'; r.why = 'already has hero'; results.push(r); console.log(`SKIP ${slug.padEnd(22)} already has hero`); continue; }
+ const dir = path.join(PROJECTS, slug);
+ const idxPath = path.join(dir, 'public', 'index.html');
+ if (!fs.existsSync(idxPath)) { r.status = 'SKIP'; r.why = 'no public/index.html'; results.push(r); console.log(`SKIP ${slug.padEnd(22)} no public/index.html`); continue; }
+
+ const hero = pickHero(slug, ledger.usedUrls);
+ if (!hero) { r.status = 'SKIP'; r.why = 'pool exhausted'; results.push(r); continue; }
+
+ sh(`git checkout -B style-rebuild-pilot`, dir);
+
+ fs.copyFileSync(COMPONENT, path.join(dir, 'public', 'dw-header.js'));
+ r.steps.push('header-component');
+
+ let html = fs.readFileSync(idxPath, 'utf8');
+ const a = auditBySlug[slug] || {};
+ const accent = (a.expected && String(a.expected).startsWith('#')) ? a.expected : '#9B7B4F';
+ const name = titleCase(slug);
+ if (!html.includes('/dw-header.js')) {
+ const cfg = `<script>window.DwHeaderConfig={siteName:${JSON.stringify(name)},accent:${JSON.stringify(accent)},`
+ + `navLinks:[{name:'Home',url:'/'},{name:'Collections',url:'#shop'},{name:'About',url:'/about'},{name:'Care',url:'/care.html'},{name:'History',url:'/history.html'}],`
+ + `adminLinks:[{name:'Shopify admin',url:'https://admin.shopify.com',external:true},{name:'Edit hero',url:'#'},{name:'Analytics',url:'#'}]};</script>\n`
+ + `<script src="/dw-header.js" defer></script>\n`;
+ html = html.replace('</body>', cfg + '</body>');
+ r.steps.push('header-injected');
+ }
+ if (html.includes('src="/corner-nav.js"')) {
+ html = html.replace(/<script src="\/corner-nav\.js"[^>]*><\/script>/g, '<!-- corner-nav disabled (UL-hamburger rebuild) -->');
+ r.steps.push('corner-nav-off');
+ }
+ if (a.status === 'DRIFT' && String(a.expected).startsWith('#') && a.actualAccent && a.actualAccent.startsWith('#')) {
+ const re = new RegExp('(--accent\\s*:\\s*)' + a.actualAccent.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi');
+ const before = html; html = html.replace(re, '$1' + a.expected);
+ r.steps.push(html !== before ? `accent ${a.actualAccent}->${a.expected}` : 'accent-skip (pattern not found)');
+ } else { r.steps.push('accent-skip (mono/neutral or no drift)'); }
+ fs.writeFileSync(idxPath, html);
+
+ execSync(`curl -s --max-time 25 "${hero.base}?width=2400" -o "${path.join(dir,'public','hero-bg.jpg')}"`, { stdio:'ignore' });
+ const dims = probeDims(path.join(dir,'public','hero-bg.jpg'));
+ if (dims.w < 1200 || dims.h < 1200) { throw new Error(`hero too small ${dims.w}x${dims.h}`); }
+ r.hero = `${dims.w}x${dims.h}`; r.heroTier = hero.tier; r.steps.push(`hero-${hero.tier}`);
+
+ sh(`git add -A`, dir);
+ sh(`git -c user.email="steve@designerwallcoverings.com" -c user.name="Steve Abrams" commit -q -m "fleet rebuild (gap-fill): dw-header (hamburgers UL + centered name) + unique high-res hero + accent — local branch"`, dir);
+ r.commit = sh(`git rev-parse --short HEAD`, dir);
+
+ // merge-safe ledger write: re-read from disk, add ours, write back.
+ ledger = readLedger();
+ ledger.usedUrls[hero.url] = slug;
+ ledger.sites[slug] = { url: hero.url, dims, tier: hero.tier };
+ fs.writeFileSync(LEDGER_PATH, JSON.stringify(ledger, null, 2) + '\n');
+ r.status = 'DONE';
+ } catch (e) {
+ r.status = 'FAIL'; r.why = String(e.message || e).split('\n')[0].slice(0, 160);
+ }
+ results.push(r);
+ console.log(`${(r.status||'?').padEnd(5)} ${slug.padEnd(22)} ${r.hero||''} ${r.heroTier||''} ${r.commit||''} ${r.why||''}`);
+}
+
+fs.writeFileSync(path.join(SELF, 'fill-gap-report.json'), JSON.stringify({ ranAt: new Date().toISOString(), results }, null, 2) + '\n');
+const done = results.filter(r=>r.status==='DONE').length;
+console.log(`\n${done}/${results.length} gap sites filled · report: fill-gap-report.json`);
diff --git a/hero-ledger.json b/hero-ledger.json
index 2721509..ae84eaa 100644
--- a/hero-ledger.json
+++ b/hero-ledger.json
@@ -40,7 +40,14 @@
"https://cdn.shopify.com/s/files/1/0015/4117/7456/files/Design_by_Suzanne_Kasler_Photo_by_Mali_Azima_6ce7d2f1-68b1-462c-bac6-7ee0aae5853d.png?v=1778629855": "silverleafwallpaper",
"https://cdn.shopify.com/s/files/1/0015/4117/7456/files/57fca922955460e7141db8b5efe6157d.png?v=1775520060": "textilewallpaper",
"https://cdn.shopify.com/s/files/1/0015/4117/7456/files/ae71b2ea71b5be686b7131abd389435b.png?v=1775520079": "vinylwallpaper",
- "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/inw-1499-sample-anjuna-textured-vinyl-hollywood-wallcoverings.jpg?v=1775698677": "wallpapercanada"
+ "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/inw-1499-sample-anjuna-textured-vinyl-hollywood-wallcoverings.jpg?v=1775698677": "wallpapercanada",
+ "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/ScreenShot2023-11-02at1.49.14PM_b0447210-fc3d-4f83-9065-2611647e3907.png": "1900swallpaper",
+ "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/EWP-1110_15ddd439-ceb2-49fa-a80e-d0442b04b93a.jpg": "blockprintedwallpaper",
+ "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/bark-swatch-VBK107-1-1.jpg": "corkwallcovering",
+ "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T20817_c5229c88-1810-4b2b-b27d-5cbc0f5face5.jpg": "hospitalitywallpaper",
+ "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/CHINA_SEAS_WALLPAPER_DWC_716300_Charleston_II_New_Blue_on_Off_White_300864W_jpg_88a75a9a-ffac-4a8e-867c-b7c8c65b248c.jpg": "screenprintedwallpaper",
+ "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/35516_16_3aab0565-33cc-44b1-aa08-ded1f1aae49b.jpg": "silkwallpaper",
+ "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/20240626_131221.jpg": "suedewallpaper"
},
"sites": {
"jutewallpaper": {
@@ -329,6 +336,123 @@
"w": 2400,
"h": 2400
}
+ },
+ "1900swallpaper": {
+ "url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/ScreenShot2023-11-02at1.49.14PM_b0447210-fc3d-4f83-9065-2611647e3907.png",
+ "dims": {
+ "w": 1586,
+ "h": 1580
+ }
+ },
+ "blockprintedwallpaper": {
+ "url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/EWP-1110_15ddd439-ceb2-49fa-a80e-d0442b04b93a.jpg",
+ "dims": {
+ "w": 2400,
+ "h": 2624
+ }
+ },
+ "corkwallcovering": {
+ "url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/bark-swatch-VBK107-1-1.jpg",
+ "dims": {
+ "w": 1600,
+ "h": 2267
+ }
+ },
+ "hospitalitywallpaper": {
+ "url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T20817_c5229c88-1810-4b2b-b27d-5cbc0f5face5.jpg",
+ "dims": {
+ "w": 2400,
+ "h": 2400
+ }
+ },
+ "screenprintedwallpaper": {
+ "url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/CHINA_SEAS_WALLPAPER_DWC_716300_Charleston_II_New_Blue_on_Off_White_300864W_jpg_88a75a9a-ffac-4a8e-867c-b7c8c65b248c.jpg",
+ "dims": {
+ "w": 2400,
+ "h": 3200
+ }
+ },
+ "silkwallpaper": {
+ "url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/35516_16_3aab0565-33cc-44b1-aa08-ded1f1aae49b.jpg",
+ "dims": {
+ "w": 2400,
+ "h": 2400
+ }
+ },
+ "suedewallpaper": {
+ "url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/20240626_131221.jpg",
+ "dims": {
+ "w": 2400,
+ "h": 2431
+ }
+ },
+ "embroideredwallpaper": {
+ "gradient": true,
+ "hue": 43
+ },
+ "handcraftedwallpaper": {
+ "gradient": true,
+ "hue": 337
+ },
+ "recycledwallpaper": {
+ "gradient": true,
+ "hue": 122
+ },
+ "stringwallpaper": {
+ "gradient": true,
+ "hue": 186
+ },
+ "animate-museum-posts": {
+ "gradient": true,
+ "hue": 147
+ },
+ "bleachresistantfabrics": {
+ "gradient": true,
+ "hue": 339
+ },
+ "fabricfriday": {
+ "gradient": true,
+ "hue": 99
+ },
+ "museumwallpaper": {
+ "gradient": true,
+ "hue": 121
+ },
+ "textiletuesday": {
+ "gradient": true,
+ "hue": 223
+ },
+ "wallpaperdefinitions": {
+ "gradient": true,
+ "hue": 21
+ },
+ "wallpaperdictionary": {
+ "gradient": true,
+ "hue": 25
+ },
+ "wallpaperdistributors": {
+ "gradient": true,
+ "hue": 31
+ },
+ "wallpaperestimator": {
+ "gradient": true,
+ "hue": 85
+ },
+ "wallpaperglossary": {
+ "gradient": true,
+ "hue": 105
+ },
+ "wallpaperhistory": {
+ "gradient": true,
+ "hue": 81
+ },
+ "wallpaperwednesday": {
+ "gradient": true,
+ "hue": 3
+ },
+ "wallsandfabrics": {
+ "gradient": true,
+ "hue": 25
}
}
}
← a6469f0 full-fleet fan-out: 41/65 sites rebuilt (header + unique hig
·
back to Dw Fleet Registry
·
close out fleet: header-only on 24, niche-search heroes for aa6a541 →