← back to Ventura Corridor

public/this-week.html

154 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>This week on the Boulevard · Ventura Corridor</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400;1,500&family=Inter:wght@200;300;400;500;600&family=JetBrains+Mono:wght@200;300;400&display=swap');
:root {
  --paper:#faf6ee;--ink:#1a1815;--ink-mute:#6e6356;--metal:#8a6d3b;
  --metal-glow:#b89968;--accent:#6a3a1a;--rule:#d8cdb8;
  --serif:'Cormorant Garamond',Georgia,serif;--sans:'Inter',sans-serif;--mono:'JetBrains Mono',monospace;
}
html,body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);font-weight:300}
*{box-sizing:border-box}
a{color:var(--ink);text-decoration:none}
a:hover{color:var(--accent)}

header.masthead{text-align:center;padding:56px 24px 32px;border-bottom:4px double var(--ink)}
.masthead .kicker{font-weight:500;font-size:10px;letter-spacing:.4em;text-transform:uppercase;color:var(--metal);margin-bottom:14px}
.masthead h1{font-family:var(--serif);font-style:italic;font-weight:500;font-size:64px;line-height:.95;margin:0;letter-spacing:-.02em}
.masthead h1 em{font-style:normal;color:var(--metal)}
.masthead .deck{font-family:var(--serif);font-weight:300;font-style:italic;font-size:17px;color:var(--ink-mute);margin-top:14px;max-width:600px;margin-left:auto;margin-right:auto;line-height:1.5}
.masthead .issue{margin-top:20px;font-family:var(--mono);font-size:9px;letter-spacing:.3em;text-transform:uppercase;color:var(--ink-mute)}
nav.crossnav{padding:14px 32px;border-bottom:1px solid var(--rule);display:flex;gap:14px;flex-wrap:wrap;justify-content:center;font-family:var(--mono);font-size:9px;letter-spacing:.28em;text-transform:uppercase}
nav.crossnav a{color:var(--ink-mute);padding:5px 12px;border:1px solid var(--rule)}
nav.crossnav a:hover,nav.crossnav a.active{color:var(--metal);border-color:var(--metal)}

main{max-width:1100px;margin:0 auto;padding:48px 32px}

.lead-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:32px;margin-bottom:64px;padding-bottom:48px;border-bottom:1px solid var(--rule)}
@media (max-width:840px){.lead-grid{grid-template-columns:1fr}}
.lead{display:flex;flex-direction:column;gap:12px}
.lead .biz{font-family:var(--mono);font-size:9px;letter-spacing:.28em;text-transform:uppercase;color:var(--metal)}
.lead h2{font-family:var(--serif);font-style:italic;font-weight:500;font-size:30px;line-height:1.15;margin:0;color:var(--ink)}
.lead .body{font-family:var(--serif);font-size:15px;line-height:1.55;color:var(--ink-mute);margin:0}
.lead .body em{color:var(--accent);font-style:italic}
.lead .meta{font-family:var(--mono);font-size:9px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute);margin-top:auto;padding-top:14px;border-top:1px solid var(--rule);display:flex;justify-content:space-between;gap:14px}
.lead .meta a{color:var(--metal);border-bottom:1px dotted var(--metal)}
.lead a.read-more{color:var(--accent);font-family:var(--sans);font-weight:500;font-size:12px;letter-spacing:.08em;text-transform:uppercase}

.tail-section h3{font-family:var(--serif);font-style:italic;font-weight:500;font-size:22px;margin:0 0 6px;color:var(--ink)}
.tail-section .deck-small{font-family:var(--serif);font-weight:300;font-style:italic;font-size:13px;color:var(--ink-mute);margin:0 0 24px}
.tail{display:grid;grid-template-columns:repeat(2,1fr);gap:18px 32px}
@media (max-width:840px){.tail{grid-template-columns:1fr}}
.tail-item{padding:14px 0;border-bottom:1px solid var(--rule);display:flex;flex-direction:column;gap:6px}
.tail-item .biz{font-family:var(--mono);font-size:9px;letter-spacing:.22em;text-transform:uppercase;color:var(--metal)}
.tail-item h4{font-family:var(--serif);font-weight:500;font-size:18px;line-height:1.25;margin:0;color:var(--ink)}
.tail-item .meta{font-family:var(--mono);font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-mute)}

.empty{padding:80px 0;text-align:center;color:var(--ink-mute);font-family:var(--serif);font-style:italic;font-size:20px}

footer.colophon{text-align:center;padding:48px 24px 64px;border-top:4px double var(--ink);color:var(--ink-mute);font-family:var(--serif);font-style:italic;font-size:13px}
</style>
</head>
<body>

<header class="masthead">
  <p class="kicker">Ventura Corridor · Issue dispatched</p>
  <h1>This <em>week</em> on the Boulevard</h1>
  <p class="deck">Fresh signals from every storefront with a website on Ventura Bl — what was announced, posted, opened, or changed in the last seven days, served straight from the source.</p>
  <p class="issue" id="issue-line">loading…</p>
</header>

<nav class="crossnav">
  <a href="/">Map</a>
  <a href="/magazine.html">Magazine</a>
  <a href="/this-week.html" class="active">This week</a>
  <a href="/news.html">News</a>
  <a href="/calendar.html">Calendar</a>
  <a href="/coverage.html">Coverage</a>
</nav>

<main>
  <div id="content">
    <div class="empty">Loading the boulevard's week…</div>
  </div>
</main>

<footer class="colophon">
  Edited by qwen3:14b on a Mac in Sherman Oaks · printed at <span id="now">…</span>
</footer>

<script>
const escHtml = s => String(s||'').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]);
const fmt = s => {
  if (!s) return '';
  const d = new Date(s);
  if (isNaN(d.getTime())) return '';
  return d.toLocaleDateString('en-US', { weekday:'short', month:'short', day:'numeric' });
};
const truncate = (s, n) => {
  if (!s) return '';
  return s.length > n ? s.slice(0, n - 1).replace(/\s+\S*$/, '') + '…' : s;
};
function hostOf(u) { try { return new URL(u).hostname.replace(/^www\./, ''); } catch { return ''; } }

function renderLead(r) {
  const text = r.summary || r.excerpt || '';
  const date = r.published_guess || r.fetched_at;
  return [
    '<article class="lead">',
      '<p class="biz">', escHtml(r.business_name||''), r.city ? ' · ' + escHtml(r.city) : '', '</p>',
      '<h2>', escHtml(truncate(r.title || '(untitled)', 90)), '</h2>',
      '<p class="body">', escHtml(truncate(text, 320)), '</p>',
      '<div class="meta"><span>', escHtml(fmt(date)), '</span>',
        '<a href="', escHtml(r.source_url), '" target="_blank" rel="noopener noreferrer">', escHtml(hostOf(r.source_url)), ' ↗</a>',
      '</div>',
    '</article>'
  ].join('');
}
function renderTail(r) {
  const date = r.published_guess || r.fetched_at;
  return [
    '<a class="tail-item" href="', escHtml(r.source_url), '" target="_blank" rel="noopener noreferrer">',
      '<span class="biz">', escHtml(r.business_name||''), '</span>',
      '<h4>', escHtml(truncate(r.title || '(untitled)', 100)), '</h4>',
      '<span class="meta">', escHtml(fmt(date)), ' · ', escHtml(hostOf(r.source_url)), '</span>',
    '</a>'
  ].join('');
}

async function load() {
  try {
    const r = await fetch('/api/news/digest');
    const d = await r.json();
    const content = document.getElementById('content');
    if (!d.lead || !d.lead.length) {
      content.innerHTML = '<div class="empty">No fresh news yet. Check back after tonight\'s scrape.</div>';
      return;
    }
    const total = (d.lead.length || 0) + (d.tail.length || 0);
    document.getElementById('issue-line').textContent = total + ' stories · ' + fmt(d.generated_at);

    let html = '<div class="lead-grid">' + d.lead.map(renderLead).join('') + '</div>';
    if (d.tail.length) {
      html += '<section class="tail-section">';
      html += '<h3>Also this week</h3>';
      html += '<p class="deck-small">' + d.tail.length + ' more dispatches from the corridor.</p>';
      html += '<div class="tail">' + d.tail.map(renderTail).join('') + '</div>';
      html += '</section>';
    }
    content.innerHTML = html;
  } catch (e) {
    document.getElementById('content').innerHTML = '<div class="empty">Could not load. Is the server up?</div>';
  }
}

document.getElementById('now').textContent = new Date().toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' });
load();
</script>
</body>
</html>