← back to Domain Landings

lib/generate.js

94 lines

// Deterministic, $0 per-domain content generator.
// Parses a domain name -> words -> inferred category -> themed one-pager config.
// No LLM, no network: same domain always yields the same page (clean diffs, cache-friendly).

const KNOWN = ['abrams','beverly','hills','butler','boulevard','without','student','debt',
  'wallpaper','wallcoverings','wallpapers','rentals','awards','news','daily','atlas','civic',
  'directory','guide','index','industries','intel','intelligence','live','local','maps','markets',
  'os','protection','space','terminal','vc','videos','crisis','center','hotline','voter','attorney',
  'lawyers','forgive','loan','borrower','bills','petition','your','no','worries','cypres','as','seen',
  'in','hotels','movies','showrooms','la','best','actor','documentary','films','born','bh','bar',
  'stock','lining','blank','bleach','friendly','ai','ask','nonna','all'];

// split a domain label into words (greedy dictionary match, fallback to camel/simple)
function words(label) {
  const s = label.toLowerCase();
  const out = []; let i = 0;
  while (i < s.length) {
    let matched = '';
    for (const w of KNOWN) {
      if (s.startsWith(w, i) && w.length > matched.length) matched = w;
    }
    if (matched) { out.push(matched); i += matched.length; }
    else { // consume until next known word starts, or one char
      let j = i + 1;
      while (j < s.length && !KNOWN.some(w => s.startsWith(w, j))) j++;
      out.push(s.slice(i, j)); i = j;
    }
  }
  return out.filter(Boolean);
}

const CATS = [
  { key: 'concierge', kw: ['butler','concierge'], palette: ['#0e1b2a','#c9a24b'],
    kicker: 'Luxury Concierge', blurb: 'A premium on-demand concierge & errand service concept — vetted, discreet, always on.',
    features: ['On-demand tasks & errands','Vetted local professionals','Members-first priority scheduling'] },
  { key: 'directory', kw: ['directory','index','guide','atlas','maps','local','markets','intel','intelligence'], palette: ['#10233b','#4aa3df'],
    kicker: 'Curated Directory', blurb: 'A curated directory & discovery platform concept — organized, searchable, and trusted.',
    features: ['Structured, searchable listings','Verified & ranked results','Local & category coverage'] },
  { key: 'media', kw: ['news','daily','videos','films','documentary','movies','live','allnews'], palette: ['#1a1a1f','#e0533d'],
    kicker: 'Media & Stories', blurb: 'An independent media & storytelling concept — timely, original, worth your attention.',
    features: ['Original reporting & features','Video-first storytelling','Daily curated briefings'] },
  { key: 'awards', kw: ['awards','best','actor','cypres'], palette: ['#141019','#d4af37'],
    kicker: 'Recognition & Awards', blurb: 'An awards & recognition program concept — honoring the best in its field.',
    features: ['Juried recognition program','Nominee showcase','Annual honors & events'] },
  { key: 'realestate', kw: ['rentals','born','bh','beverly','hills','boulevard'], palette: ['#0f1e17','#7bb274'],
    kicker: 'Property & Living', blurb: 'A local property, rentals & lifestyle concept for a sought-after market.',
    features: ['Featured listings','Neighborhood guides','Concierge leasing'] },
  { key: 'wallcovering', kw: ['wallpaper','wallcoverings','wallpapers','bar','stock','lining','bleach'], palette: ['#1c1614','#b8865f'],
    kicker: 'Surfaces & Design', blurb: 'A specialty wallcovering & surface-design concept — materials, patterns, and inspiration.',
    features: ['Curated pattern library','Trade & designer focus','Samples & specification'] },
  { key: 'tech', kw: ['os','terminal','space','protection','ai','industries','civic','vc','petition','your'], palette: ['#0b1220','#5b8def'],
    kicker: 'Platform & Tools', blurb: 'A modern platform concept — built to be fast, secure, and genuinely useful.',
    features: ['Purpose-built workflows','Privacy-first by design','Built to scale'] },
];

function categorize(ws) {
  for (const c of CATS) if (ws.some(w => c.kw.includes(w))) return c;
  return { key: 'brand', kw: [], palette: ['#15171c','#9aa7b4'],
    kicker: 'Available Concept', blurb: 'A distinctive, brandable domain ready for its next chapter.',
    features: ['Short, memorable, brandable','Clean search & voice presence','Ready to build on'] };
}

const title = ws => ws.map(w => w ? w[0].toUpperCase() + w.slice(1) : w).join(' ');

function build(domain) {
  const label = domain.replace(/\.[a-z]+$/i, '');
  const ws = words(label);
  const cat = categorize(ws);
  return {
    domain,
    title: title(ws) || label,
    kicker: cat.kicker,
    category: cat.key,
    tagline: cat.blurb,
    features: cat.features,
    palette: { bg: cat.palette[0], accent: cat.palette[1] },
    ga4: '',            // filled per-domain after GA4 property creation
  };
}

module.exports = { build, words, title };

if (require.main === module) {
  const fs = require('fs');
  const list = JSON.parse(fs.readFileSync(__dirname + '/../data/undeveloped.json', 'utf8'));
  const cfg = {};
  for (const d of list) cfg[d] = build(d);
  fs.writeFileSync(__dirname + '/../data/domains.json', JSON.stringify(cfg, null, 2));
  const counts = {};
  for (const d in cfg) counts[cfg[d].category] = (counts[cfg[d].category] || 0) + 1;
  console.log(`built ${Object.keys(cfg).length} domain configs`);
  console.log('by category:', counts);
}