← back to Ventura Corridor
iter 129+130: /about masthead page + /api/feedback POST + reader widget — /about.html public 'About the desk' page (Cormorant 84px italic, 'What this is' + 'How it's made' + 'Why we started' sections w/ drop-cap lede + colophon dl-grid showing editor/model/typeface/stack/source/frequency/volume + ink-on-paper contact card mailto:steve); CREATE TABLE reader_feedback (id, feature_id, kind, body, contact, source_path, received_at, reviewed); POST /api/feedback validates kind in (note/typo/correction/tip/complaint/praise) + 4-2000 char body; GET /api/feedback returns last 100 w/ feature joined; /magazine/:id reader gets a collapsible '<details> Notice a typo' feedback form at the bottom (kind dropdown + body textarea + optional email) that POSTs ajax-style and replaces with green 'Got it. Thanks for the note.' on success
04c6a9590c6ac1c14ce0735197eb8cd2d45159fb · 2026-05-06 18:39:05 -0700 · SteveStudio2
Files touched
A public/about.htmlM src/server/index.ts
Diff
commit 04c6a9590c6ac1c14ce0735197eb8cd2d45159fb
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Wed May 6 18:39:05 2026 -0700
iter 129+130: /about masthead page + /api/feedback POST + reader widget — /about.html public 'About the desk' page (Cormorant 84px italic, 'What this is' + 'How it's made' + 'Why we started' sections w/ drop-cap lede + colophon dl-grid showing editor/model/typeface/stack/source/frequency/volume + ink-on-paper contact card mailto:steve); CREATE TABLE reader_feedback (id, feature_id, kind, body, contact, source_path, received_at, reviewed); POST /api/feedback validates kind in (note/typo/correction/tip/complaint/praise) + 4-2000 char body; GET /api/feedback returns last 100 w/ feature joined; /magazine/:id reader gets a collapsible '<details> Notice a typo' feedback form at the bottom (kind dropdown + body textarea + optional email) that POSTs ajax-style and replaces with green 'Got it. Thanks for the note.' on success
---
public/about.html | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++
src/server/index.ts | 71 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 164 insertions(+)
diff --git a/public/about.html b/public/about.html
new file mode 100644
index 0000000..57d7763
--- /dev/null
+++ b/public/about.html
@@ -0,0 +1,93 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>About · The 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,400;1,500&family=Inter:wght@300;400;500&family=JetBrains+Mono:wght@200;300&display=swap');
+:root{--paper:#faf6ee;--ink:#1a1815;--ink-mute:#6e6356;--metal:#8a6d3b;--metal-glow:#b89968;--accent:#6a3a1a;--rule:#d8cdb8}
+*{box-sizing:border-box}
+html,body{margin:0;background:var(--paper);color:var(--ink);font-family:'Inter',sans-serif;font-weight:300;line-height:1.65}
+header{text-align:center;padding:64px 24px 36px;border-bottom:4px double var(--ink)}
+header .kicker{font-size:10px;letter-spacing:.45em;text-transform:uppercase;color:var(--metal);font-weight:500}
+header h1{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:84px;line-height:.95;margin:14px 0 0;letter-spacing:-0.02em}
+header h1 em{font-style:normal;color:var(--metal)}
+main{max-width:680px;margin:0 auto;padding:48px 24px 80px}
+section{margin-bottom:36px}
+section h2{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:30px;margin:0 0 12px;color:var(--ink)}
+section h2 em{font-style:normal;color:var(--metal)}
+section p{font-size:16px;color:var(--ink);margin:0 0 14px}
+section .lede::first-letter{font-family:'Cormorant Garamond',serif;font-weight:600;font-size:72px;line-height:.85;float:left;margin:6px 12px 0 0;color:var(--accent)}
+
+.colophon{margin-top:60px;padding:32px 28px;background:rgba(184,153,104,0.06);border:1px solid var(--rule);font-size:14px}
+.colophon h3{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:20px;margin:0 0 14px;color:var(--metal)}
+.colophon dl{display:grid;grid-template-columns:auto 1fr;gap:10px 18px;font-family:'Inter',sans-serif}
+.colophon dt{color:var(--ink-mute);font-size:9px;letter-spacing:.32em;text-transform:uppercase;font-weight:500;align-self:center}
+.colophon dd{margin:0;font-family:'Cormorant Garamond',serif;font-size:16px;font-style:italic;color:var(--ink)}
+.colophon dd code{font-family:'JetBrains Mono',monospace;font-size:12px;font-style:normal;background:var(--paper);padding:2px 8px;border:1px solid var(--rule)}
+
+.contact{margin-top:32px;padding:24px;background:var(--ink);color:var(--paper);text-align:center}
+.contact p{color:rgba(255,255,255,0.85);font-size:14px;margin:0 0 14px}
+.contact a{color:var(--metal-glow);text-decoration:none;font-family:'JetBrains Mono',monospace;font-size:13px;border-bottom:1px solid var(--metal-glow);padding-bottom:1px}
+.contact a:hover{color:white;border-color:white}
+
+blockquote{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:300;font-size:24px;line-height:1.4;color:var(--accent);border-left:3px solid var(--metal);padding:14px 0 14px 22px;margin:24px 0}
+
+.toolbar{position:fixed;top:14px;right:14px;display:flex;gap:6px;z-index:100}
+.toolbar a{font-size:9px;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-mute);background:var(--paper);padding:5px 10px;border:1px solid var(--rule);text-decoration:none}
+.toolbar a:hover{color:var(--metal);border-color:var(--metal)}
+</style>
+</head>
+<body>
+<div class="toolbar">
+ <a href="/issue">Read the issue</a>
+ <a href="/find.html">Find a business</a>
+ <a href="/rate-card.html">Sponsor</a>
+</div>
+<header>
+ <div class="kicker">The Corridor · masthead</div>
+ <h1>About <em>the desk</em></h1>
+</header>
+<main>
+
+<section>
+ <h2>What this <em>is</em>.</h2>
+ <p class="lede">The Corridor is a quarterly magazine of every business on Ventura Boulevard — Sherman Oaks through Woodland Hills — written warm. Every restaurant, professional, salon, atelier, and shop along nine miles of corridor gets a feature. We don't take pitches; we don't run press releases; we just walk the boulevard and write what's there.</p>
+</section>
+
+<section>
+ <h2>How it's <em>made</em>.</h2>
+ <p>Each draft starts from public license records — addresses, business names, the trade categories the city files. From there our editorial assistant (a model called <code style="background:rgba(184,153,104,0.08);padding:2px 8px;font-family:'JetBrains Mono',monospace;font-size:12px">qwen3:14b</code>, running on a Mac Studio under the desk) writes a first ~100 words. The desk reads, trims, and presses publish.</p>
+ <p>We're not in business with the businesses. Sponsorship slots exist (see <a href="/rate-card.html" style="color:var(--metal)">rate card</a>) but the editorial isn't for sale — sponsorship buys placement and a custom photo, not a different opinion.</p>
+</section>
+
+<blockquote>"The walls of Ventura Boulevard tell you who's here. The Corridor writes them down."</blockquote>
+
+<section>
+ <h2>Why we <em>started</em>.</h2>
+ <p>The corridor is the most-trafficked retail spine in Los Angeles north of the 10. The number of independent businesses surviving on it after a decade of e-commerce is itself a story. We wanted a record — not a Yelp clone, not a Yellow Pages, not a directory — a magazine.</p>
+ <p>If you live here, this is your shopping guide. If you don't, this is your travel guide. If you're a business owner: yours is in here too. Send us a photo and we'll typeset it.</p>
+</section>
+
+<div class="colophon">
+ <h3>Colophon</h3>
+ <dl>
+ <dt>Editor</dt><dd>Steve Abrams</dd>
+ <dt>Editorial assistant</dt><dd><code>qwen3:14b</code> on Mac Studio 1</dd>
+ <dt>Type</dt><dd>Cormorant Garamond · Inter · JetBrains Mono</dd>
+ <dt>Stack</dt><dd><code>Express + Postgres + Playwright + Ollama</code></dd>
+ <dt>Source data</dt><dd>City of LA BTRC license registry</dd>
+ <dt>Frequency</dt><dd>Quarterly · digital-first · print-on-demand</dd>
+ <dt>Volume</dt><dd>I · Spring · 2026</dd>
+ </dl>
+</div>
+
+<div class="contact">
+ <p>Notes, corrections, sponsor questions, story tips:</p>
+ <a href="mailto:steve@designerwallcoverings.com?subject=The%20Corridor">steve@designerwallcoverings.com</a>
+</div>
+
+</main>
+</body>
+</html>
diff --git a/src/server/index.ts b/src/server/index.ts
index f043f1c..c249cc5 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -87,6 +87,8 @@ const ADMIN_PATHS = [
/^\/rate-card(\.html)?\/?$/i,
/^\/sponsor(\.html)?\/?$/i,
/^\/api\/sponsor\/inquiries\/?$/i,
+ /^\/about(\.html)?\/?$/i,
+ /^\/api\/feedback\/?$/i,
/^\/sponsor\/\d+\/?$/i,
/^\/api\/sponsor(\/.*)?$/i,
/^\/crawl-derby(\.html)?\/?$/i,
@@ -2889,6 +2891,41 @@ ${b.notes ? `<div style="margin-top:24px;padding:14px 18px;background:rgba(184,1
}
});
+// Reader feedback intake — typo reports, business-owner corrections, story tips.
+app.post('/api/feedback', express.json(), async (req, res) => {
+ try {
+ const b = req.body || {};
+ const kind = String(b.kind || 'note').slice(0, 32);
+ if (!b.body || String(b.body).length < 4) return res.status(400).json({ error: 'body required (min 4 chars)' });
+ if (String(b.body).length > 5000) return res.status(400).json({ error: 'body too long (max 5000 chars)' });
+ if (!['note','typo','correction','tip','complaint','praise'].includes(kind)) return res.status(400).json({ error: 'unknown kind' });
+ const r = await query(
+ `INSERT INTO reader_feedback (feature_id, kind, body, contact, source_path)
+ VALUES ($1, $2, $3, $4, $5) RETURNING id`,
+ [b.feature_id || null, kind, b.body, b.contact || null, b.source_path || null]
+ );
+ res.json({ ok: true, id: r.rows[0].id });
+ } catch (e: any) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
+app.get('/api/feedback', async (_req, res) => {
+ try {
+ const r = await query(
+ `SELECT f.id, f.feature_id, f.kind, f.body, f.contact, f.received_at, f.reviewed,
+ mf.headline, b.name AS biz_name
+ FROM reader_feedback f
+ LEFT JOIN magazine_features mf ON mf.id = f.feature_id
+ LEFT JOIN businesses b ON b.id = mf.business_id
+ ORDER BY f.received_at DESC LIMIT 100`
+ );
+ res.json({ count: r.rowCount, rows: r.rows });
+ } catch (e: any) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
app.patch('/api/sponsor/inquiries/:id', express.json(), async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
@@ -3238,6 +3275,40 @@ footer{margin-top:48px;padding:24px 0;text-align:center;font-size:10px;letter-sp
</a>`).join('')}
</div>
</section>` : ''}
+ <details style="margin-top:48px;padding-top:24px;border-top:1px solid var(--rule)">
+ <summary style="cursor:pointer;font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--ink-mute);font-family:'Inter',sans-serif">Notice a typo, fact slip, or want to add something?</summary>
+ <form id="fb-form-${id}" style="margin-top:16px;display:flex;flex-direction:column;gap:10px" onsubmit="return submitFb(event, ${id})">
+ <select name="kind" style="background:transparent;border:1px solid var(--rule);color:var(--ink);padding:8px 10px;font-family:'Inter',sans-serif;font-size:12px">
+ <option value="typo">Typo or copy edit</option>
+ <option value="correction">Factual correction</option>
+ <option value="tip">Story tip / what we missed</option>
+ <option value="praise">Just kind words</option>
+ <option value="complaint">Concern or take-down request</option>
+ <option value="note">Other</option>
+ </select>
+ <textarea name="body" required minlength="4" maxlength="2000" placeholder="What did you notice?" style="background:transparent;border:1px solid var(--rule);color:var(--ink);padding:10px 12px;font-family:'Cormorant Garamond',serif;font-size:14px;min-height:80px;resize:vertical;line-height:1.5"></textarea>
+ <input name="contact" type="text" placeholder="Email (optional, only if you want a reply)" style="background:transparent;border:1px solid var(--rule);color:var(--ink);padding:8px 12px;font-family:'Inter',sans-serif;font-size:12px">
+ <button type="submit" style="background:var(--ink);color:var(--paper);border:none;padding:10px 24px;font-size:10px;letter-spacing:.32em;text-transform:uppercase;font-weight:500;cursor:pointer;font-family:'Inter',sans-serif;align-self:flex-start">Send to the desk</button>
+ </form>
+ <div id="fb-thanks-${id}" style="display:none;color:var(--green,#6a9b73);font-style:italic;margin-top:12px;font-family:'Cormorant Garamond',serif;font-size:16px">Got it. Thanks for the note.</div>
+ </details>
+ <script>
+ async function submitFb(e, fid) {
+ e.preventDefault();
+ const form = document.getElementById('fb-form-' + fid);
+ const data = Object.fromEntries(new FormData(form));
+ data.feature_id = fid;
+ data.source_path = location.pathname;
+ const r = await fetch('/api/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
+ if (r.ok) {
+ form.style.display = 'none';
+ document.getElementById('fb-thanks-' + fid).style.display = 'block';
+ } else {
+ alert('Sorry — something failed. Try again?');
+ }
+ return false;
+ }
+ </script>
<footer>The Corridor · Volume I · 2026 · all loopback / not for redistribution</footer>
</article>
</body></html>`);
← 5af0acf iter 127+128: /magazine/:id/print.html print-only view + spo
·
back to Ventura Corridor
·
iter 131-132: /feedback.html admin viewer + today.html unrea 030b0b1 →