[object Object]

← 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

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 →