← back to Dw Domain Fleet

scripts/gen-configs.js

241 lines

#!/usr/bin/env node
/**
 * gen-configs.js — generate sites/<slug>.json for all 66 fleet domains.
 *
 * Each domain gets a DISTINCT visual identity (palette + font pair + niche
 * filter + copy) so the fleet is NOT 66 identical templates (doorway-page
 * SEO risk). Identity is derived from a per-domain spec table below.
 */
const fs = require('fs');
const path = require('path');

const SITES_DIR = path.join(__dirname, '..', 'sites');
fs.mkdirSync(SITES_DIR, { recursive: true });

// ---- palette library (distinct visual directions) ----
const PALETTES = {
  ivory:    { bgLight:'#faf7f2', inkLight:'#1a1714', bg:'#14110d', ink:'#f3efe8', rule:'#d8cec0', accent:'#7d3c00' },
  noir:     { bgLight:'#f4f2ee', inkLight:'#15140f', bg:'#0a0a09', ink:'#f0ede6', rule:'#cfcabf', accent:'#a8884a' },
  sage:     { bgLight:'#f3f4ee', inkLight:'#1c1f17', bg:'#10120c', ink:'#eef0e6', rule:'#cdd0c0', accent:'#5a6f3e' },
  sand:     { bgLight:'#f6f1e7', inkLight:'#211c12', bg:'#16120a', ink:'#f1ead9', rule:'#d6c9ad', accent:'#9c6b30' },
  slate:    { bgLight:'#eef0f2', inkLight:'#161a1e', bg:'#0c0e11', ink:'#e8eaee', rule:'#c4cad0', accent:'#3a5f8a' },
  bordeaux: { bgLight:'#f6f0ee', inkLight:'#1d1310', bg:'#120a08', ink:'#f0e6e2', rule:'#d4c2bb', accent:'#7a2230' },
  coastal:  { bgLight:'#f0f4f5', inkLight:'#10191c', bg:'#0a1113', ink:'#e6eef0', rule:'#bdcdd0', accent:'#2a7d8a' },
  gilt:     { bgLight:'#f7f3e8', inkLight:'#1c1810', bg:'#0d0b06', ink:'#f2ecd8', rule:'#d8caa0', accent:'#b08923' },
  blush:    { bgLight:'#f8f1f0', inkLight:'#1f1614', bg:'#130c0b', ink:'#f1e6e3', rule:'#dcc8c4', accent:'#a85a5f' },
  ink:      { bgLight:'#f1f1f0', inkLight:'#121211', bg:'#080807', ink:'#ededec', rule:'#c8c8c5', accent:'#444440' },
  forest:   { bgLight:'#eef1ec', inkLight:'#13170f', bg:'#0a0d07', ink:'#e8ece2', rule:'#c2c9b8', accent:'#2f5d3a' },
  plum:     { bgLight:'#f3eff3', inkLight:'#1a131c', bg:'#0e0a10', ink:'#ece4ee', rule:'#cdc1d0', accent:'#6a3a7a' }
};
const FONTS = {
  cormorant: { serif:"'Cormorant Garamond','Playfair Display',Georgia,serif", sans:"-apple-system,'Inter',system-ui,sans-serif" },
  playfair:  { serif:"'Playfair Display',Georgia,serif", sans:"-apple-system,'Inter',system-ui,sans-serif" },
  bodoni:    { serif:"'Bodoni Moda',Didot,Georgia,serif", sans:"-apple-system,'Helvetica Neue',Arial,sans-serif" },
  georgia:   { serif:"Georgia,'Times New Roman',serif", sans:"-apple-system,'Inter',system-ui,sans-serif" }
};

// niche filter shorthand → {pos,neg,types}
function W(t){return ['Wallcovering'];}

// ---- the 44 BUILD domains: slug, palette, font, niche, copy ----
//
// SCOPE: a dtd debate-panel ruled SPLIT (Option B) on 2026-05-18. Of the
// original 66 fleet domains, 22 near-duplicate/typo-variant domains stay as
// 301 redirects to designerwallcoverings.com (doorway-page SEO risk if built
// out) and are NOT in this table. The 44 below have genuinely distinct search
// intent and get standalone sites.
//
// niche.pos = any-match keywords; neg = exclude; types = product_type filter.
// SEO MUST-DO #1: every site serves a CURATED SUBSET (niche filter) with
// original per-site copy — never the full shared catalog rendered identically.
// Domains whose intent is geographic/editorial rather than product-category
// (asseeninla, roomsettings, etc.) are differentiated by original landing copy
// + per-site title/meta + rel=canonical, not by a product filter.
const SPEC = [
  ['1800wallcoverings',   'ivory',   'cormorant', {types:W()},                              'Designer wallcoverings for every room.'],
  ['asseeninhotels',      'gilt',    'bodoni',    {pos:['hospitality','hotel','contract','commercial']}, 'Wallcoverings seen in the world\'s finest hotels.'],
  ['asseeninla',          'noir',    'playfair',  {types:W()},                              'The wallcoverings of Los Angeles design.'],
  ['asseeninmovies',      'noir',    'bodoni',    {pos:['dramatic','glamour','scenic','statement','mural','metallic']}, 'Wallcoverings seen on the silver screen.'],
  ['asseeninshowrooms',   'slate',   'cormorant', {types:W()},                              'Showroom-grade wallcoverings, direct to the trade.'],
  ['barwallpaper',        'bordeaux','bodoni',    {pos:['bar','hospitality','dark','dramatic','jewel']}, 'Dramatic wallcoverings for bars and lounges.'],
  ['blankstocklining',    'ink',     'georgia',   {pos:['liner','blank','lining','solid','paper'],neg:['display']}, 'Liner and blankstock for a flawless finish.'],
  ['carmelwallpaper',     'sage',    'cormorant', {pos:['grasscloth','sisal','jute','organic modern','botanical']}, 'Coastal-calm wallcoverings, Carmel to Monterey.'],
  ['cfafabrics',          'blush',   'playfair',  {types:['Fabric']},                       'Designer fabrics for the trade.'],
  ['classawallcovering',  'slate',   'cormorant', {pos:['class a','fire rated','commercial','contract']}, 'Class A fire-rated wallcoverings.'],
  ['commercialsalesreps', 'ink',     'georgia',   {pos:['commercial','contract']},          'Commercial wallcoverings and trade representation.'],
  ['commercialwallcovering','slate', 'cormorant', {pos:['commercial','contract','class a']}, 'Contract-grade commercial wallcoverings.'],
  ['designermagnetics',   'slate',   'bodoni',    {pos:['magnetic','metallic','mica','metal']}, 'Magnetic and metallic wallcoverings.'],
  ['etciemurals',         'plum',    'playfair',  {pos:['mural','scenic','panoramic'],types:['Mural','Wallcovering']}, 'Statement murals for interiors.'],
  ['fabricfridays',       'blush',   'playfair',  {types:['Fabric']},                       'New designer fabrics every Friday.'],
  ['fireratedwallcovering','slate',  'georgia',   {pos:['fire rated','class a','commercial','contract']}, 'Fire-rated wallcoverings for code compliance.'],
  ['flocked',             'bordeaux','bodoni',    {pos:['flock','velvet','damask','traditional']}, 'Flocked velvet wallcoverings.'],
  ['grassclothwallcovering','sage',  'cormorant', {pos:['grasscloth','sisal','jute','arrowroot']}, 'Hand-crafted grasscloth wallcoverings.'],
  ['handmadewallcovering','sand',    'cormorant', {pos:['grasscloth','sisal','jute','hand screen','handcraft','artisan']}, 'Hand-made wallcoverings, artisan-crafted.'],
  ['hollywoodwallcovering','gilt',   'bodoni',    {pos:['hollywood','glamour','metallic','dramatic']}, 'Hollywood-glamour wallcoverings.'],
  ['hospitalitysalesagency','gilt',  'georgia',   {pos:['hospitality','hotel','contract','commercial']}, 'Hospitality wallcoverings and sales representation.'],
  ['malibuwallpaper',     'coastal', 'cormorant', {pos:['grasscloth','sisal','jute','organic modern','botanical']}, 'Coastal wallcoverings, Malibu style.'],
  ['montereywallpaper',   'sage',    'cormorant', {pos:['grasscloth','sisal','jute','organic modern']}, 'Monterey-coast wallcoverings.'],
  ['naturalwalltextures', 'sage',    'cormorant', {pos:['grasscloth','sisal','jute','arrowroot','madagascar','cork','mica']}, 'Natural-fiber wall textures.'],
  ['patterndesignlab',    'plum',    'playfair',  {pos:['pattern','geometric','floral','abstract']}, 'A laboratory of pattern design.'],
  ['philliperomano',      'noir',    'bodoni',    {pos:['phillipe romano']},                'The Phillipe Romano collection.'],
  ['printmurals',         'plum',    'playfair',  {pos:['mural','scenic','panoramic','print'],types:['Mural','Wallcovering']}, 'Printed murals for every wall.'],
  ['restaurantfabrics',   'bordeaux','playfair',  {types:['Fabric']},                       'Designer fabrics for restaurants.'],
  ['restaurantmurals',    'bordeaux','bodoni',    {pos:['mural','scenic','panoramic'],types:['Mural','Wallcovering']}, 'Murals for restaurant interiors.'],
  ['roomsettings',        'ivory',   'cormorant', {types:W()},                              'Wallcoverings, seen in the room.'],
  ['sheltermagazines',    'noir',    'playfair',  {types:W()},                              'The wallcoverings of shelter editorial.'],
  ['specifywallpaper',    'slate',   'georgia',   {pos:['commercial','contract','class a','fire rated']}, 'Specify commercial wallcoverings with confidence.'],
  ['stevenabramsphotography','ink',  'playfair',  {types:W()},                              'Steven Abrams — photography and design.'],
  ['thehotelwallpaper',   'gilt',    'bodoni',    {pos:['hospitality','hotel','contract','commercial']}, 'Wallcoverings for the modern hotel.'],
  ['traditionalwhimsy',   'blush',   'playfair',  {pos:['traditional','floral','toile','botanical','whimsical']}, 'Traditional wallcoverings with a whimsical turn.'],
  ['unitedstateswallpaper','slate',  'georgia',   {types:W()},                              'American designer wallcoverings.'],
  ['wallcovering',        'ivory',   'cormorant', {types:W()},                              'Wallcovering — the catalog.'],
  ['wallpaperexports',    'sand',    'georgia',   {types:W()},                              'Designer wallcoverings for export.'],
  ['wallpaperfromthe80s', 'plum',    'bodoni',    {pos:['retro','vintage','geometric','abstract','1980']}, 'Retro wallcoverings with 80s spirit.'],
  ['wallpaperny',         'ink',     'bodoni',    {types:W()},                              'New York designer wallcoverings.'],
  ['wallpaperpurchasing', 'slate',   'georgia',   {pos:['commercial','contract']},          'Commercial wallcovering purchasing.'],
  ['wallpaperweekly',     'ivory',   'playfair',  {types:W()},                              'New designer wallcoverings, weekly.'],
  ['wc01wallcoverings',   'slate',   'georgia',   {pos:['commercial','contract','class a']}, 'WC-01 commercial wallcoverings.']
];

// ---- the 22 REDIRECT-ONLY domains (NOT built — 301 → designerwallcoverings.com) ----
// Kept here for documentation / DNS-audit reference only; gen-configs does not
// emit a site config for these. Near-duplicate typo/variant spellings or
// redundant siblings of designerwallcoverings.com.
const REDIRECT_ONLY = [
  'designerswallcovering.com','designerswallcoverings.com','designerswallpapers.com',
  'designerwallcover.com','designerwallcovering.com','designwallcoverings.com',
  'designerwallpaints.com','designerwalltiles.com','dwlosangeles.com',
  'designerlaboratory.com','thedesignerlaboratory.com','handmadewallpapers.com',
  'handscreenedwallpaper.com','hospitalitysalesagents.com','hospitalitysalesreps.com',
  'commercialwalls.com','1800wallpapers.com','800wallpaper.com','wallpapernyc.com',
  'wc01wallpaper.com','wc-01.com','steveabramsdesigns.com'
];

// domain overrides where slug != slug+'.com'
const DOMAIN_OVERRIDE = {
  'flocked': 'flocked.org',
  'wallcovering': 'wallcovering.net',
  'wc-01': 'wc-01.com'
};

// explicit display-name overrides — for compound slugs the regex titleCase
// reads as a slug. The brand display is still the domain stem, just spaced.
const NAME_OVERRIDE = {
  'dwlosangeles':            'DW Los Angeles',
  'asseeninla':              'As Seen In LA',
  'asseeninhotels':          'As Seen In Hotels',
  'asseeninmovies':          'As Seen In Movies',
  'asseeninshowrooms':       'As Seen In Showrooms',
  'wallpaperfromthe80s':     'Wallpaper From The 80s',
  'steveabramsdesigns':      'Steve Abrams Designs',
  'stevenabramsphotography': 'Steven Abrams Photography',
  'thedesignerlaboratory':   'The Designer Laboratory',
  'thehotelwallpaper':       'The Hotel Wallpaper',
  'cfafabrics':              'CFA Fabrics',
  'hospitalitysalesagency':  'Hospitality Sales Agency',
  'hospitalitysalesagents':  'Hospitality Sales Agents',
  'hospitalitysalesreps':    'Hospitality Sales Reps',
  'commercialsalesreps':     'Commercial Sales Reps',
  'unitedstateswallpaper':   'United States Wallpaper',
  'patterndesignlab':        'Pattern Design Lab',
  'designerlaboratory':      'Designer Laboratory',
  'naturalwalltextures':     'Natural Wall Textures',
  'handscreenedwallpaper':   'Hand-Screened Wallpaper',
  'sheltermagazines':        'Shelter Magazines',
  'roomsettings':            'Room Settings',
  'traditionalwhimsy':       'Traditional Whimsy',
  'designermagnetics':       'Designer Magnetics',
  'designerwalltiles':       'Designer Wall Tiles',
  'designerwallpaints':      'Designer Wall Paints',
  'wallpaperexports':        'Wallpaper Exports',
  'wallpaperpurchasing':     'Wallpaper Purchasing',
  'wallpaperweekly':         'Wallpaper Weekly',
  'fabricfridays':           'Fabric Fridays',
  'restaurantfabrics':       'Restaurant Fabrics',
  'restaurantmurals':        'Restaurant Murals',
  'printmurals':             'Print Murals',
  'etciemurals':             'Et Cie Murals',
  'barwallpaper':            'Bar Wallpaper',
  'carmelwallpaper':         'Carmel Wallpaper',
  'malibuwallpaper':         'Malibu Wallpaper',
  'montereywallpaper':       'Monterey Wallpaper',
  'philliperomano':          'Phillipe Romano',
  'flocked':                 'Flocked',
  'blankstocklining':        'Blankstock Lining',
  'fireratedwallcovering':   'Fire-Rated Wallcovering',
  'classawallcovering':      'Class A Wallcovering',
  'specifywallpaper':        'Specify Wallpaper',
  'handmadewallcovering':    'Hand-Made Wallcovering',
  'handmadewallpapers':      'Hand-Made Wallpapers',
  'hollywoodwallcovering':   'Hollywood Wallcovering',
  'grassclothwallcovering':  'Grasscloth Wallcovering',
  'commercialwallcovering':  'Commercial Wallcovering',
  'commercialwalls':         'Commercial Walls',
  'designwallcoverings':     'Design Wallcoverings',
  'designerwallcover':       'Designer Wallcover',
  'designerwallcovering':    'Designer Wallcovering',
  'designerswallcovering':   'Designers Wallcovering',
  'designerswallcoverings':  'Designers Wallcoverings',
  'designerswallpapers':     'Designers Wallpapers',
  'wallpaperny':             'Wallpaper NY',
  'wallpapernyc':            'Wallpaper NYC',
  'wc-01':                   'WC-01',
  'wc01wallcoverings':       'WC-01 Wallcoverings',
  'wc01wallpaper':           'WC-01 Wallpaper',
  '1800wallcoverings':       '1-800 Wallcoverings',
  '1800wallpapers':          '1-800 Wallpapers',
  '800wallpaper':            '800 Wallpaper',
  'wallcovering':            'Wallcovering'
};

function titleCase(slug){
  let s = slug
    .replace(/wc-?0?1/i,'WC-01 ')
    .replace(/(wallcoverings?|wallpapers?|walls|fabrics?|magnetics|paints|tiles|murals|laboratory|exports|purchasing|weekly|agency|agents|reps|photography|designs|settings|magazines|whimsy)/gi,' $1 ')
    .replace(/(\d{3,4}s?)/g,' $1 ')
    .replace(/\b(nyc|ny|la)\b/gi,' $1 ')
    .replace(/\s+/g,' ').trim()
    .replace(/\b\w/g,c=>c.toUpperCase());
  // city/state abbreviations stay uppercase
  return s.replace(/\bNyc\b/g,'NYC').replace(/\bNy\b/g,'NY').replace(/\bLa\b/g,'LA');
}

let start = 9901;
const manifest = [];
SPEC.forEach((row, i) => {
  const [slug, pal, font, niche, tagline] = row;
  const palette = PALETTES[pal];
  const fonts = FONTS[font];
  const domain = DOMAIN_OVERRIDE[slug] || (slug + '.com');
  const siteName = NAME_OVERRIDE[slug] || titleCase(slug);
  const port = start + i;
  const nicheLabel = niche.types && niche.types[0] === 'Fabric' ? 'designer fabrics'
    : niche.types && niche.types[0] === 'Mural' ? 'designer murals'
    : 'designer wallcoverings';
  const cfg = {
    slug, domain, siteName, port,
    siteEmail: 'info@' + domain,
    tagline,
    heroHeadline: siteName,
    heroSub: 'Explore the collection',
    metaDesc: `${siteName} — ${tagline} Free memo samples, expert trade service, and hand-holding on every order from Designer Wallcoverings.`,
    nicheLabel,
    aboutCopy: `${siteName} curates ${nicheLabel} for designers, architects, and discerning homeowners. ${tagline} Every selection is backed by Designer Wallcoverings' decades of trade experience.`,
    niche: { pos: niche.pos || [], neg: niche.neg || [], types: niche.types || [] },
    theme: {
      accent: palette.accent,
      bg: palette.bg, bgLight: palette.bgLight,
      ink: palette.ink, inkLight: palette.inkLight,
      rule: palette.rule,
      serif: fonts.serif, sans: fonts.sans,
      palette: pal, font
    }
  };
  fs.writeFileSync(path.join(SITES_DIR, slug + '.json'), JSON.stringify(cfg, null, 2));
  manifest.push({ slug, domain, port, palette: pal, font, niche: niche });
});

fs.writeFileSync(path.join(__dirname, '..', 'data', 'manifest.json'), JSON.stringify(manifest, null, 2));
console.log(`Generated ${manifest.length} site configs → sites/`);
console.log(`Ports ${start}–${start + manifest.length - 1}`);