← back to Microsite Engine
auto-save: 2026-07-17T10:16:09 (2 files) — public/index.html server.js
ad0025eb6ea7affc085db550390817b2948c8ed9 · 2026-07-17 10:16:14 -0700 · Steve Abrams
Files touched
M public/index.htmlM server.js
Diff
commit ad0025eb6ea7affc085db550390817b2948c8ed9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jul 17 10:16:14 2026 -0700
auto-save: 2026-07-17T10:16:09 (2 files) — public/index.html server.js
---
public/index.html | 35 +++++++++++++++++++++++++++++++----
server.js | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 84 insertions(+), 5 deletions(-)
diff --git a/public/index.html b/public/index.html
index ce21ec5..3e27d0e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -78,10 +78,14 @@
<!-- ============ FORM ============ -->
<div class="form">
<div id="fleetErr"></div>
+ <label>Start from a site idea <span class="note">(owned unused domain × catalog niche)</span></label>
+ <select id="ideaAdd"></select>
<label>Slug (subdomain, a–z0–9-)</label>
- <input id="slug" placeholder="corkwallcovering" autocomplete="off">
- <label>Domain</label>
- <input id="domain" placeholder="corkwallcovering.com" autocomplete="off">
+ <input id="slug" placeholder="corkwallcovering" autocomplete="off" list="slugList">
+ <datalist id="slugList"></datalist>
+ <label>Domain <span class="note">(dropdown = owned domains not yet in the fleet)</span></label>
+ <input id="domain" placeholder="corkwallcovering.com" autocomplete="off" list="domainList">
+ <datalist id="domainList"></datalist>
<label>Site name</label>
<input id="siteName" placeholder="Cork Wallcovering" autocomplete="off">
<label>Tagline</label>
@@ -206,6 +210,29 @@ async function boot(){
if(chip) chip.classList.add('on');
doPreview();
};
+ // slug + domain dropdowns, prefilled from the owned-domain portfolio
+ // (CNCP, minus domains the live fleet already uses)
+ const doms = FACETS.domains || [];
+ $('domainList').innerHTML = doms.map(d=>`<option value="${esc(d)}"></option>`).join('');
+ $('slugList').innerHTML = doms.map(d=>`<option value="${esc(d.split('.')[0].replace(/[^a-z0-9-]/g,''))}"></option>`).join('');
+ // picking an owned domain auto-derives the slug (if still empty)
+ $('domain').addEventListener('change', ()=>{
+ const d=$('domain').value.trim().toLowerCase();
+ if(doms.includes(d) && !$('slug').value.trim()) $('slug').value=d.split('.')[0].replace(/[^a-z0-9-]/g,'');
+ });
+ // "start from a site idea" — each idea pairs an owned unused domain with the
+ // catalog tags found inside its name (real product counts)
+ $('ideaAdd').innerHTML = '<option value="">💡 pick an idea to prefill the form…</option>' +
+ (FACETS.ideas||[]).map((it,i)=>`<option value="${i}">${esc(it.domain)} — ${esc(it.keywords.join(', '))} · ${it.count.toLocaleString()} products</option>`).join('');
+ $('ideaAdd').onchange = ()=>{
+ const it=(FACETS.ideas||[])[+$('ideaAdd').value];
+ if(!it) return;
+ $('slug').value=it.slug; $('domain').value=it.domain; $('siteName').value=it.siteName;
+ $('tagline').value=`Curated ${it.keywords[0].toLowerCase()} wallcoverings for designers and discerning homes.`;
+ $('pos').value=it.keywords.join(', ');
+ document.querySelectorAll('#tagChips .chip').forEach(c=>c.classList.toggle('on',it.keywords.some(k=>k.toLowerCase()===c.dataset.tag.toLowerCase())));
+ syncHeroText(); doPreview();
+ };
// typed-suggestion autocomplete for the include/exclude keyword fields,
// prefilled from the REAL catalog vocabulary (all tags ∪ all product types)
const VOCAB=[...new Set([...FACETS.tagList.map(t=>t.name), ...FACETS.typeList.map(t=>t.name)])];
@@ -328,7 +355,7 @@ async function generate(){
$('sort').addEventListener('change',doPreview);
$('density').addEventListener('input',e=>document.documentElement.style.setProperty('--cols',e.target.value));
$('genBtn').onclick=generate;
-$('resetBtn').onclick=()=>{['slug','domain','siteName','tagline','heroHeadline','heroSub','pos','neg'].forEach(id=>$(id).value='');selTypes.clear();document.querySelectorAll('.chip.on').forEach(c=>c.classList.remove('on'));syncHeroText();doPreview();};
+$('resetBtn').onclick=()=>{['slug','domain','siteName','tagline','heroHeadline','heroSub','pos','neg'].forEach(id=>$(id).value='');$('ideaAdd').value='';selTypes.clear();document.querySelectorAll('.chip.on').forEach(c=>c.classList.remove('on'));syncHeroText();doPreview();};
boot().then(syncHeroText);
</script>
</body></html>
diff --git a/server.js b/server.js
index bb302a8..6acfd0a 100644
--- a/server.js
+++ b/server.js
@@ -59,6 +59,58 @@ function buildFacets() {
}
const FACETS = buildFacets();
+// ---- owned-domain portfolio (READ-ONLY from CNCP) + computed site ideas ----
+// The point of this tool is pairing a domain Steve ALREADY OWNS with a catalog
+// niche. So the slug/domain fields get a dropdown of owned, non-expired domains
+// not yet used by the live fleet, and an "ideas" dropdown pairs each such domain
+// with the catalog tags its name contains (real product counts, $0 local).
+const CNCP_CONFIG = process.env.CNCP_CONFIG || path.join(process.env.HOME, 'cncp-starter', 'cncp-config.json');
+function ownedDomains() {
+ try {
+ const c = JSON.parse(fs.readFileSync(CNCP_CONFIG, 'utf8'));
+ return (c.domains || [])
+ .filter(d => d.name && d.status !== 'expired')
+ .map(d => d.name.toLowerCase());
+ } catch { return []; }
+}
+// greedy longest-match segmentation of a domain base ("corkwallcovering" →
+// ["cork","wallcovering"]) against the catalog vocabulary + trade filler words
+const FILLER_WORDS = ['wallcoverings', 'wallcovering', 'wallpapers', 'wallpaper', 'designer', 'walls', 'wall', 'the', 'my', 'shop', 'and'];
+function segmentBase(base, vocab) {
+ const words = [];
+ let rest = base;
+ while (rest.length) {
+ const hit = vocab.find(w => rest.startsWith(w));
+ if (!hit) { words.push(rest); break; }
+ words.push(hit);
+ rest = rest.slice(hit.length);
+ }
+ return words;
+}
+function domainIdeas() {
+ const taken = new Set(liveSites().map(s => (s.domain || '').toLowerCase()));
+ const free = ownedDomains().filter(d => !taken.has(d));
+ const tagByLower = new Map(FACETS.tagList.map(t => [t.name.toLowerCase(), t]));
+ const vocab = [...new Set([...tagByLower.keys(), ...FILLER_WORDS])]
+ .filter(w => w.length >= 3).sort((a, b) => b.length - a.length);
+ const ideas = [];
+ for (const dom of free) {
+ const base = dom.split('.')[0].replace(/[^a-z0-9]/g, '');
+ const words = segmentBase(base, vocab);
+ const tags = words.map(w => tagByLower.get(w)).filter(Boolean);
+ if (!tags.length) continue;
+ ideas.push({
+ domain: dom,
+ slug: base,
+ siteName: words.map(w => w[0].toUpperCase() + w.slice(1)).join(' '),
+ keywords: tags.map(t => t.name),
+ count: Math.max(...tags.map(t => t.n)),
+ });
+ }
+ ideas.sort((a, b) => b.count - a.count);
+ return { domains: free, ideas: ideas.slice(0, 60) };
+}
+
// ---- read the LIVE fleet's existing sites (READ-ONLY) for collision guard ----
function liveSites() {
const dir = path.join(FLEET, 'sites');
@@ -214,7 +266,7 @@ const server = http.createServer(async (req, res) => {
return send(res, 200, 'text/html; charset=utf-8', fs.readFileSync(path.join(__dirname, 'public', 'index.html')));
}
if (p === '/api/facets') {
- return json(res, 200, { ...FACETS, fleetOk: FLEET_OK, fleetErr: FLEET_ERR, palettes: Object.keys(PALETTES), fonts: Object.keys(FONTS) });
+ return json(res, 200, { ...FACETS, ...domainIdeas(), fleetOk: FLEET_OK, fleetErr: FLEET_ERR, palettes: Object.keys(PALETTES), fonts: Object.keys(FONTS) });
}
if (p === '/api/preview' && req.method === 'POST') {
return json(res, 200, preview(await readBody(req)));
← e01e000 chore: lint, refactor, v0.2.1 (session close)
·
back to Microsite Engine
·
prefilled dropdowns for slug + domain + site ideas (owned CN 3071de6 →