← back to Small Business Builder

src/render/fresh.js

151 lines

// /fresh — newest state-licensed shops first. The BBCB Issue Date field is the
// "registered with the state" timestamp — a salon's grand-opening signal. This
// page lists the freshest 100 by issue date, perfect for a weekly-refresh
// page that pulls repeat traffic + ranks for "newly opened salon" / "new
// barbershop in {neighborhood}" long-tail queries.

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

function fmtDate(s) {
  // BBCB stores either "MM-DD-YYYY" or "MM/DD/YYYY" — render as "Mar 14, 2025"
  const m = String(s || '').match(/^(\d{1,2})[-/](\d{1,2})[-/](\d{4})$/);
  if (!m) return s || '';
  const [, mm, dd, yyyy] = m;
  const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  const idx = parseInt(mm, 10) - 1;
  if (idx < 0 || idx > 11) return s;
  return `${months[idx]} ${parseInt(dd, 10)}, ${yyyy}`;
}

function slugify(s) {
  return String(s || '').toLowerCase().replace(/[^\w]+/g, '-').replace(/^-|-$/g, '');
}

export function renderFresh({ businesses = [] } = {}) {
  const baseUrl = process.env.SITE_URL || 'https://localhost';
  const collectionPage = {
    '@context': 'https://schema.org',
    '@type': 'CollectionPage',
    name: 'New & Newly-Licensed Salons in LA',
    description: 'The 100 most-recently licensed salons, barbershops, and spas across LA + Orange + Riverside + San Bernardino + Ventura counties.',
    url: `${baseUrl}/fresh`,
    dateModified: new Date().toISOString(),
    mainEntity: {
      '@type': 'ItemList',
      numberOfItems: businesses.length,
      itemListElement: businesses.slice(0, 100).map((b, i) => ({
        '@type': 'ListItem',
        position: i + 1,
        item: {
          '@type': 'HairSalon',
          name: b.name,
          url: `${baseUrl}/biz/${b.slug}`,
        },
      })),
    },
  };

  return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Just Opened · Newest LA Salons & Barbershops · LA Salon Directory</title>
<meta name="description" content="The freshest state-licensed salons, barbers, and spas across the LA region — sorted by Board of Barbering & Cosmetology issue date.">
<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">
<script type="application/ld+json">${escJsonLd(JSON.stringify(collectionPage))}</script>
<style>
:root{
  --ink:#1a1a1a;--paper:#f5f5f5;--rule:#e6e3dc;--mute:#7a766f;
  --accent:#a16207; /* warm gold-amber for "fresh / new" */
  --gold:#b8945a;
  --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}

.topbar{padding:18px 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:60;flex-wrap:wrap;gap:12px}
.brand{font-family:var(--serif);font-size:20px;letter-spacing:-.01em;font-weight:500}
.brand em{font-style:italic;color:var(--accent)}
.topnav{display:flex;gap:18px;font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute)}
.topnav a:hover{color:var(--ink)}

.hero{padding:80px 32px 48px;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(--accent);margin-bottom:18px}
.hero h1{font-family:var(--serif);font-weight:500;font-size:clamp(48px,7vw,96px);line-height:.96;letter-spacing:-.02em;margin-bottom:20px;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:62ch;margin-bottom:28px}
.hero-meta{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute)}

.fresh-list{padding:48px 32px;max-width:1240px;margin:0 auto}
.fresh-row{display:grid;grid-template-columns:120px 1fr 200px 120px;gap:32px;align-items:baseline;padding:22px 0;border-bottom:1px solid var(--rule);transition:padding 180ms ease}
.fresh-row:hover{padding-left:8px;background:#fff}
.fresh-date{font-family:var(--mono);font-size:11px;letter-spacing:.06em;color:var(--accent);text-transform:uppercase;font-weight:500}
.fresh-name{font-family:var(--serif);font-size:24px;font-weight:500;letter-spacing:-.005em;line-height:1.1}
.fresh-name em{font-style:italic;color:var(--mute);font-size:13px;margin-left:8px}
.fresh-where{font-family:var(--mono);font-size:11px;letter-spacing:.06em;color:var(--mute);text-transform:uppercase;line-height:1.5}
.fresh-lic{font-family:var(--mono);font-size:9px;letter-spacing:.16em;color:var(--mute);text-align:right}
.fresh-lic a{padding:4px 8px;border:1px solid var(--rule);transition:all 150ms ease}
.fresh-lic a:hover{background:var(--accent);color:var(--paper);border-color:var(--accent)}
@media (max-width:880px){
  .fresh-row{grid-template-columns:80px 1fr;gap:16px}
  .fresh-where,.fresh-lic{grid-column:2;font-size:10px}
}

.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:880px){
  .topbar{padding:14px 20px}
  .hero{padding:48px 20px 32px}
  .fresh-list{padding:32px 20px}
}
</style>
</head>
<body>
<header class="topbar">
  <div class="brand"><a href="/">LA <em>Salon</em> Directory</a> <span style="color:var(--mute);font-style:italic">/ just opened</span></div>
  <nav class="topnav">
    <a href="/neighborhoods">Neighborhoods</a>
    <a href="/proprietors">Proprietors</a>
    <a href="/corridor/ventura-blvd">Ventura</a>
    <a href="/corridor/sunset-blvd">Sunset</a>
    <a href="/map">Map</a>
    <a href="/">Home</a>
  </nav>
</header>

<section class="hero">
  <div class="hero-eyebrow">Fresh · State-Licensed · This Quarter</div>
  <h1>Just <em>opened.</em></h1>
  <p class="hero-lede">The 100 most-recent state-licensed salons, barbers, and spas across the LA region — sorted by California Board of Barbering &amp; Cosmetology issue date. New chairs, new lobbies, new colorists.</p>
  <div class="hero-meta">${businesses.length} listings · sourced from CA DCA · refreshed ${new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</div>
</section>

<section class="fresh-list">
  ${businesses.map(b => {
    const issued = b.source_data_json?.issue_date || '';
    const lic = b.source_data_json?.license_number || '';
    const licUrl = lic ? `https://search.dca.ca.gov/results?lic=${encodeURIComponent(lic)}` : '';
    return `
    <div class="fresh-row">
      <span class="fresh-date">${esc(fmtDate(issued))}</span>
      <a class="fresh-name" href="/biz/${esc(b.slug)}">${esc(b.name)}<em>${esc(b.category || '')}</em></a>
      <span class="fresh-where">${esc(b.neighborhood || b.city || '')}</span>
      <span class="fresh-lic">${licUrl ? `<a href="${esc(licUrl)}" target="_blank" rel="noopener">◆ ${esc(lic)}</a>` : ''}</span>
    </div>`;
  }).join('')}
</section>

<footer class="footer">
  <span>LA Salon Directory · ${new Date().getFullYear()}</span>
  <span>Source: CA Department of Consumer Affairs · BBCB</span>
  <span><a href="/">← Home</a></span>
</footer>
</body>
</html>`;
}