← back to Small Business Builder

src/render/home.js

422 lines

// Public editorial homepage — LA salon directory.
// North star: Kinfolk × Soho House × neighborhood barber crew.
// Neighborhood-led primary nav, Barber/Beard featured first, no booking-platform vibes.

import { esc } from '../lib/escape.js';

// Per-neighborhood accent palette from design/north-star.md.
const NEIGHBORHOOD_ACCENT = {
  'Silver Lake':    '#1f6f70',
  'Echo Park':      '#c95a2c',
  'Koreatown':      '#c9a23a',
  'K-Town':         '#c9a23a',
  'Highland Park':  '#7a3b3b',
  'DTLA':           '#3a3a3a',
  'West Hollywood': '#b6395f',
  'Venice':         '#2c5d8a',
  'Beverly Hills':  '#8a7448',
  'Hollywood':      '#7a2530',
  'Sherman Oaks':   '#5a6e3a',
  'Santa Monica':   '#3a5a6e',
  'Culver City':    '#6e5a3a',
  'Los Feliz':      '#4a6e3a',
  'Westwood':       '#5a3a6e',
  'West Adams':     '#6e3a4a',
  'Los Angeles':    '#3a3a3a',
};

const NEIGHBORHOOD_TAGLINE = {
  'Silver Lake':    "Silver Lake's Legacy Cuts",
  'Echo Park':      'East Side Classics',
  'Highland Park':  'Highland Park Heritage',
  'Koreatown':      'K-Town Fades',
  'K-Town':         'K-Town Fades',
  'DTLA':           'Downtown Modern',
  'West Hollywood': 'WeHo Polish',
  'Venice':         'Salt-Air Sessions',
  'Beverly Hills':  'Beverly Hills Standard',
  'Hollywood':      'Hollywood, Established',
  'Sherman Oaks':   'Valley Quiet',
  'Santa Monica':   'Westside Routine',
  'Culver City':    'Culver Craft',
  'Los Feliz':      'Los Feliz Living',
};

// Order neighborhoods for nav by editorial priority, falling back to alphabetical.
const NAV_ORDER = [
  'Silver Lake', 'Echo Park', 'Highland Park', 'Koreatown', 'DTLA',
  'West Hollywood', 'Venice', 'Beverly Hills', 'Hollywood',
  'Sherman Oaks', 'Santa Monica', 'Culver City', 'Los Feliz',
];

const CATEGORY_LABEL = {
  barbershop: 'Barber & Beard',
  beard:      'Barber & Beard',
  salon:      'Hair',
  hair:       'Hair',
  nail:       'Nails',
  nail_salon: 'Nails',
  lash:       'Lash',
  brow:       'Brow',
  spa:        'Spa',
  blowout:    'Blowout',
  waxing:     'Waxing',
  generic:    'Salon',
};

function neighborhoodSlug(n) {
  return String(n || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
}

function accent(neighborhood) {
  return NEIGHBORHOOD_ACCENT[neighborhood] || '#1f6f70';
}

function tagline(neighborhood) {
  return NEIGHBORHOOD_TAGLINE[neighborhood] || `${neighborhood}, by appointment`;
}

function categoryLabel(c) {
  return CATEGORY_LABEL[c] || (c ? c[0].toUpperCase() + c.slice(1) : 'Salon');
}

// Render a single shop card. Editorial — image-led, accent rule, sparse copy.
function renderShopCard(b) {
  const a = accent(b.neighborhood);
  const hero = b.hero_image_url || '';
  const cat = categoryLabel(b.category);
  return `
  <a class="shop" href="/biz/${esc(b.slug)}" style="--accent:${a}">
    <div class="shop-img">${hero ? `<img loading="lazy" src="${esc(hero)}" alt="${esc(b.name)}">` : `<div class="shop-img-placeholder"></div>`}</div>
    <div class="shop-meta">
      <span class="shop-cat">${esc(cat)}</span>
      <h3 class="shop-name">${esc(b.name)}</h3>
      <p class="shop-loc">${esc(b.neighborhood || b.city || 'Los Angeles')}</p>
    </div>
  </a>`;
}

function renderNeighborhoodTile(n) {
  const a = accent(n.neighborhood);
  const slug = neighborhoodSlug(n.neighborhood);
  return `
  <a class="hood" href="/n/${esc(slug)}" style="--accent:${a}">
    <span class="hood-name">${esc(n.neighborhood)}</span>
    <span class="hood-meta"><em>${esc(tagline(n.neighborhood))}</em> · ${n.count} shop${n.count === 1 ? '' : 's'}</span>
  </a>`;
}

// Build the featured-interview block, if any.
// Pull the philosophy + best neighborhood-section answer (favorite restaurant
// or coffee — these read best as a teaser) and link to the full profile.
function renderFeaturedInterview(featured) {
  if (!featured || !featured.business || !featured.qa_json) return '';
  const b = featured.business;
  const qa = featured.qa_json;
  const philosophy = qa.philosophy;
  // Pick the most appealing teaser from the neighborhood section.
  const teaserKey = ['fav_restaurant','best_coffee','after_the_chair','hidden_gem','date_night']
    .find(k => qa[k] && qa[k].trim());
  const teaserMap = {
    fav_restaurant:  'Where they actually eat',
    best_coffee:     'Best coffee, no diplomacy',
    after_the_chair: '30 minutes to kill, post-cut',
    hidden_gem:      'Hidden gem, no tourists',
    date_night:      'Where to take a date',
  };
  if (!philosophy && !teaserKey) return '';
  const accent = NEIGHBORHOOD_ACCENT[b.neighborhood] || '#1f6f70';
  return `
  <section class="featured" style="--feat-accent:${accent}">
    <div class="featured-inner">
      <div class="featured-eyebrow">In this issue · <a href="/biz/${esc(b.slug)}">${esc(b.name)}</a> · ${esc(b.neighborhood || 'Los Angeles')}</div>
      ${philosophy ? `
        <blockquote class="featured-quote">"${esc(philosophy)}"</blockquote>
        <div class="featured-attr">— ${esc(b.owner_name || b.name)}</div>
      ` : ''}
      ${teaserKey ? `
        <div class="featured-teaser">
          <div class="featured-teaser-label">${esc(teaserMap[teaserKey])}</div>
          <p class="featured-teaser-text">${esc(qa[teaserKey])}</p>
        </div>
      ` : ''}
      <a class="featured-cta" href="/biz/${esc(b.slug)}">Read the full interview →</a>
    </div>
  </section>`;
}

// businesses: full list, neighborhoods: [{neighborhood, count}], featuredBarbers: shops where category in barbershop/beard
export function renderHome({ featuredBarbers = [], otherShops = [], neighborhoods = [], featured = null, totalCount = 0, latestFinds = [] } = {}) {
  // Sort neighborhood list by editorial priority, then by count desc, then alphabetical
  const orderIdx = (n) => {
    const i = NAV_ORDER.indexOf(n);
    return i === -1 ? 999 : i;
  };
  const sortedHoods = [...neighborhoods].sort((a, b) => {
    const ai = orderIdx(a.neighborhood);
    const bi = orderIdx(b.neighborhood);
    if (ai !== bi) return ai - bi;
    if (a.count !== b.count) return b.count - a.count;
    return a.neighborhood.localeCompare(b.neighborhood);
  });

  return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>LA Barber & Salon Directory · By Neighborhood</title>
<meta name="description" content="Barber, beard, hair, nails, lashes, brows. By neighborhood. Curated.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
  --ink:#1a1a1a;
  --paper:#f5f5f5;
  --rule:#e6e3dc;
  --mute:#7a766f;
  --accent:#1f6f70;
  --serif:"Cormorant Garamond",Georgia,serif;
  --sans:"Inter",-apple-system,BlinkMacSystemFont,system-ui,sans-serif;
  --mono:ui-monospace,"SF Mono",Menlo,monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{background:var(--paper);color:var(--ink);font-family:var(--sans);font-weight:400;-webkit-font-smoothing:antialiased}
a{color:inherit;text-decoration:none}
img{display:block;max-width:100%;height:auto}

/* ==== Topbar ==================================================== */
.topbar{padding:22px 32px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--rule);position:sticky;top:0;background:var(--paper);z-index:50}
.brand{font-family:var(--serif);font-size:22px;letter-spacing:-.01em;font-weight:500}
.brand em{font-style:italic;color:var(--accent)}
.nav-mini{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute);display:flex;gap:24px}
.nav-mini a:hover{color:var(--ink)}

/* ==== Hero ====================================================== */
.hero{padding:120px 32px 80px;border-bottom:1px solid var(--rule);max-width:1240px;margin:0 auto}
.hero-eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--mute);margin-bottom:20px}
.hero h1{font-family:var(--serif);font-weight:500;font-size:clamp(48px,7.5vw,108px);line-height:.96;letter-spacing:-.02em;margin-bottom:28px;max-width:14ch}
.hero h1 em{font-style:italic;color:var(--accent)}
.hero-lede{font-size:18px;line-height:1.55;color:#3a3a3a;max-width:54ch;margin-bottom:48px}
.hero-stat{display:flex;gap:48px;font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute)}
.hero-stat b{display:block;font-family:var(--serif);font-style:italic;font-size:34px;letter-spacing:-.01em;color:var(--ink);font-weight:500;margin-top:4px;text-transform:none}

/* ==== Section heading =========================================== */
.section{padding:80px 32px;max-width:1240px;margin:0 auto}
.section-head{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:40px;border-bottom:1px solid var(--rule);padding-bottom:18px}
.section-head h2{font-family:var(--serif);font-weight:500;font-size:clamp(32px,4vw,52px);letter-spacing:-.015em;line-height:1}
.section-head .kicker{font-family:var(--mono);font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--mute)}

/* ==== Corridor promo strip ====================================== */
.corridor-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--rule);border:1px solid var(--rule);overflow:hidden}
.corridor-tile{background:var(--paper);padding:48px 36px 40px;border-top:4px solid var(--c-accent,var(--accent));position:relative;transition:background 220ms ease;display:flex;flex-direction:column;gap:16px;min-height:340px}
.corridor-tile:hover{background:#fff}
.corridor-tile .ct-label{font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:var(--c-accent,var(--accent));font-weight:600}
.corridor-tile .ct-title{font-family:var(--serif);font-size:clamp(38px,4.5vw,56px);font-weight:500;line-height:.96;letter-spacing:-.015em;margin:6px 0}
.corridor-tile .ct-title em{font-style:italic;color:var(--c-accent,var(--accent))}
.corridor-tile .ct-sub{font-size:14px;line-height:1.55;color:#3a3a3a;margin-bottom:auto}
.corridor-tile .ct-cta{font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:var(--c-accent,var(--accent));margin-top:18px;padding-top:14px;border-top:1px solid var(--rule);transition:letter-spacing 200ms ease}
.corridor-tile:hover .ct-cta{letter-spacing:.28em}
@media (max-width:880px){.corridor-strip{grid-template-columns:1fr}}

/* ==== Neighborhood tiles ======================================== */
.hoods{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1px;background:var(--rule);border:1px solid var(--rule)}
.hood{background:var(--paper);padding:28px 24px;display:flex;flex-direction:column;justify-content:flex-end;min-height:180px;border-left:4px solid var(--accent);transition:background 200ms ease,transform 200ms ease}
.hood:hover{background:#fff;transform:translateY(-2px)}
.hood-name{font-family:var(--serif);font-size:28px;font-weight:500;letter-spacing:-.01em;line-height:1.1;margin-bottom:6px}
.hood-meta{font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute)}
.hood-meta em{font-style:italic;color:var(--accent);text-transform:none;letter-spacing:0;font-family:var(--serif);font-size:13px}

/* ==== Shop grid ================================================= */
.shops{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:32px}
.shop{display:block;color:var(--ink)}
.shop-img{aspect-ratio:4/5;overflow:hidden;background:#222;margin-bottom:14px;position:relative}
.shop-img img{width:100%;height:100%;object-fit:cover;transition:transform 600ms ease}
.shop:hover .shop-img img{transform:scale(1.04)}
.shop-img-placeholder{position:absolute;inset:0;background:linear-gradient(135deg,var(--ink) 0%,#2a2a2a 100%);display:flex;align-items:center;justify-content:center}
.shop-img-placeholder::before{content:"";width:1px;height:36%;background:var(--accent)}
.shop-cat{font-family:var(--mono);font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);margin-bottom:6px;display:inline-block}
.shop-name{font-family:var(--serif);font-size:22px;font-weight:500;letter-spacing:-.01em;line-height:1.15;margin-bottom:4px}
.shop-loc{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--mute)}

/* ==== Featured interview ======================================== */
.featured{padding:80px 32px;background:var(--ink);color:var(--paper);border-top:1px solid var(--ink);border-bottom:1px solid var(--ink)}
.featured-inner{max-width:880px;margin:0 auto}
.featured-eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:#aaa;margin-bottom:32px}
.featured-eyebrow a{color:var(--feat-accent,var(--accent));border-bottom:1px solid var(--feat-accent,var(--accent));padding-bottom:1px}
.featured-quote{font-family:var(--serif);font-style:italic;font-size:clamp(28px,3.6vw,46px);line-height:1.2;letter-spacing:-.01em;font-weight:400;margin-bottom:18px}
.featured-quote::first-letter{color:var(--feat-accent,var(--accent));font-style:normal;font-size:1.3em}
.featured-attr{font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:#888;margin-bottom:48px}
.featured-teaser{margin-bottom:36px;padding-left:24px;border-left:2px solid var(--feat-accent,var(--accent))}
.featured-teaser-label{font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:var(--feat-accent,var(--accent));margin-bottom:10px}
.featured-teaser-text{font-family:var(--serif);font-size:21px;line-height:1.5;color:#e6e3dc}
.featured-cta{display:inline-block;font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--feat-accent,var(--accent));border:1px solid var(--feat-accent,var(--accent));padding:14px 22px}
.featured-cta:hover{background:var(--feat-accent,var(--accent));color:var(--ink)}

/* ==== Manifesto strip =========================================== */
.manifesto{padding:80px 32px;background:var(--ink);color:var(--paper);border-top:1px solid var(--ink)}
.manifesto-inner{max-width:880px;margin:0 auto}
.manifesto p{font-family:var(--serif);font-style:italic;font-size:clamp(28px,3.5vw,42px);line-height:1.25;letter-spacing:-.01em;font-weight:400}
.manifesto p em{color:#c9a23a;font-style:normal}
.manifesto .sig{font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:#aaa;margin-top:32px}

/* ==== Map pill ================================================== */
.hero-map-pill{
  display:inline-block;margin-top:32px;padding:12px 20px;
  font-family:var(--mono);font-size:10px;letter-spacing:.18em;text-transform:uppercase;
  border:1px solid var(--accent);color:var(--accent);
  transition:background 150ms,color 150ms;
}
.hero-map-pill:hover{background:var(--accent);color:var(--paper)}
.map-pill{color:var(--accent)!important}
.map-pill:hover{color:var(--ink)!important}

/* ==== Footer ==================================================== */
.footer{padding:48px 32px;border-top:1px solid var(--rule);font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--mute);display:flex;justify-content:space-between;flex-wrap:wrap;gap:16px}

@media (max-width:720px){
  .hero{padding:64px 24px 48px}
  .section{padding:48px 24px}
  .topbar{padding:16px 20px}
  .nav-mini{display:none}
  .hero-stat{flex-wrap:wrap;gap:24px}
}
</style>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebSite","name":"LA Salon Directory","url":"https://localhost/","potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://localhost/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}</script>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"Organization","name":"LA Salon Directory","url":"https://localhost/","sameAs":[]}</script>
</head>
<body>

<header class="topbar">
  <div class="brand">LA <em>Salon</em> Directory</div>
  <nav class="nav-mini">
    <a href="/best">★ The Best</a>
    <a href="/leaderboard">Leaderboard</a>
    <a href="/neighborhoods">Neighborhoods</a>
    <a href="/addresses">Addresses</a>
    <a href="/press">Press</a>
    <a href="/timeline">Live Feed</a>
    <a href="/near" class="map-pill">📍 Near Me →</a>
  </nav>
</header>

<section class="hero">
  <div class="hero-eyebrow">Barber · Beard · Hair · Nails · Lash · Brow · Spa</div>
  <h1>Barber & Salon, <em>by neighborhood.</em></h1>
  <p class="hero-lede">A directory of the LA shops worth knowing — the legacy cuts, the East-side classics, the K-Town fades, the WeHo polish. Curated by the people who actually live here. Booking optional. Authority required.</p>
  <div class="hero-stat">
    <div>Shops indexed<b>${totalCount}</b></div>
    <div>Neighborhoods<b>${neighborhoods.length}</b></div>
    <div>Categories<b>${new Set(featuredBarbers.concat(otherShops).map(s => s.category).filter(Boolean)).size}</b></div>
  </div>
  <a href="/map" class="hero-map-pill">View the whole map →</a>
</section>

${renderFeaturedInterview(featured)}

${latestFinds.length ? `
<section class="section" id="latest" style="padding-bottom:32px">
  <div class="section-head">
    <h2>Just discovered</h2>
    <a href="/timeline" class="kicker" style="color:var(--accent);text-decoration:none">Live feed →</a>
  </div>
  <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1px;background:var(--rule);border:1px solid var(--rule);overflow:hidden">
    ${latestFinds.map(f => {
      const ageMs = Date.now() - new Date(f.ts).getTime();
      const ago = ageMs < 60_000 ? 'just now' : ageMs < 3_600_000 ? Math.floor(ageMs/60_000) + 'm ago' : ageMs < 86_400_000 ? Math.floor(ageMs/3_600_000) + 'h ago' : Math.floor(ageMs/86_400_000) + 'd ago';
      const tagBg = f.kind === 'article' ? '#b8945a' : 'var(--accent)';
      const tagFg = f.kind === 'article' ? '#1a1a1a' : '#fafaf6';
      const icon  = f.kind === 'article' ? '📰' : '📷';
      const subtitle = f.kind === 'article' ? (f.publisher || 'press') : `@${f.title || f.publisher}`;
      return `<a href="/biz/${esc(f.slug)}" style="background:var(--paper);padding:18px 18px 14px;display:flex;flex-direction:column;gap:6px;transition:background 200ms ease;color:var(--ink);text-decoration:none" onmouseover="this.style.background='#fff'" onmouseout="this.style.background='var(--paper)'">
        <div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
          <span style="font-family:var(--mono);font-size:9px;letter-spacing:.18em;text-transform:uppercase;color:${tagFg};background:${tagBg};padding:2px 8px;font-weight:600">${icon} ${esc(f.kind === 'article' ? 'press' : f.publisher || 'social')}</span>
          <span style="font-family:var(--mono);font-size:9px;color:var(--mute);letter-spacing:.04em;text-transform:uppercase">${esc(ago)}</span>
        </div>
        <div style="font-family:var(--serif);font-size:18px;font-weight:500;line-height:1.2;letter-spacing:-.005em;margin-top:6px">${esc(f.name)}</div>
        <div style="font-family:var(--mono);font-size:10px;letter-spacing:.04em;color:var(--mute);text-transform:uppercase">${esc(f.neighborhood || '')}${f.neighborhood && subtitle ? ' · ' : ''}${esc(subtitle)}</div>
      </a>`;
    }).join('')}
  </div>
</section>
` : ''}

<section class="section corridors" id="corridors">
  <div class="section-head">
    <h2>The Corridors</h2>
    <span class="kicker">Three streets · Walk every block</span>
  </div>
  <div class="corridor-strip">
    <a class="corridor-tile" href="/corridor/ventura-blvd" style="--c-accent:#5a6e3a">
      <span class="ct-label">No. 1 · 20 miles</span>
      <h3 class="ct-title">Ventura <em>Boulevard</em></h3>
      <p class="ct-sub">Woodland Hills → Studio City. The valley spine — medical-spa belts, old-school dining rows, every state-licensed salon on the strip.</p>
      <span class="ct-cta">West to East · <a href="/corridor/ventura-blvd/map" onclick="event.stopPropagation()" style="color:inherit;text-decoration:underline">Drive Map →</a></span>
    </a>
    <a class="corridor-tile" href="/corridor/sunset-blvd" style="--c-accent:#c2410c">
      <span class="ct-label">No. 2 · 22 miles</span>
      <h3 class="ct-title">Sunset <em>Boulevard</em></h3>
      <p class="ct-sub">Echo Park → Pacific Ocean. The strip that built Hollywood — Walk of Fame, the Whisky, the Roxy, every salon between Vine and the Palisades.</p>
      <span class="ct-cta">East to West →</span>
    </a>
    <a class="corridor-tile" href="/corridor/wilshire-blvd" style="--c-accent:#6b3a8e">
      <span class="ct-label">No. 3 · 16 miles</span>
      <h3 class="ct-title">Wilshire <em>Boulevard</em></h3>
      <p class="ct-sub">DTLA → Santa Monica. The main street — MacArthur Park, Miracle Mile, the Beverly Hills crossing, everything along LACMA's mile.</p>
      <span class="ct-cta">East to West →</span>
    </a>
  </div>
</section>

<section class="section" id="neighborhoods">
  <div class="section-head">
    <h2>Browse by neighborhood</h2>
    <span class="kicker">${sortedHoods.length} areas covered</span>
  </div>
  <div class="hoods">
    ${sortedHoods.map(renderNeighborhoodTile).join('')}
  </div>
</section>

${featuredBarbers.length ? `
<section class="section" id="barber">
  <div class="section-head">
    <h2>Barber & Beard</h2>
    <span class="kicker">Top of the list</span>
  </div>
  <div class="shops">
    ${featuredBarbers.map(renderShopCard).join('')}
  </div>
</section>
` : ''}

<section class="manifesto">
  <div class="manifesto-inner">
    <p>Not a booking platform. Not a portfolio gallery. <em>An editorial directory</em> for the LA shops worth your standing appointment.</p>
    <div class="sig">— Editor's note</div>
  </div>
</section>

${otherShops.length ? `
<section class="section" id="all">
  <div class="section-head">
    <h2>The full list</h2>
    <span class="kicker">Hair · Nails · Lash · Brow · Spa</span>
  </div>
  <div class="shops">
    ${otherShops.map(renderShopCard).join('')}
  </div>
</section>
` : ''}

<footer class="footer">
  <span>LA Salon Directory · ${new Date().getFullYear()}</span>
  <span>Curated, not crowdsourced</span>
  <span><a href="/admin">Admin</a> · <a href="/builder">Builder</a></span>
</footer>

</body>
</html>`;
}