← 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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[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>