β back to Ventura Corridor
iter 97+98+99+100: PDF export + reader-search + IG share-card + nav surfacing β /issue.pdf?cat= renders /issue or /issue/:cat via Playwright headless chromium with httpCredentials auth, prints Letter format with 0.5/0.4in margins, attachment download w/ dated filename; /search.html standalone reader-facing search w/ Cormorant 54px italic 'Search the issue' masthead + vertical nav + 24px italic input + highlight-matched substrings + 180-char snippet + per-card category + sponsor-candidate marker; /share/:id 1080Γ1920 9:16 IG-story share-card w/ category-gradient bg + 140px italic Cormorant headline + 42px subhead + 48px pull-quote + biz/address/city footer + The Corridor mast bottom-right (auto-scales to viewport for preview, full-size for screenshot/print); /magazine/:id reader toolbar gets 'π± IG share-card' link; /magazine.html nav grows 'π΅ rate card' + 'π search' shortcuts
b3c77b475523bf00b477fbcec3b5345bd2d27578 Β· 2026-05-06 17:22:11 -0700 Β· SteveStudio2
Files touched
M public/magazine.htmlA public/search.htmlM src/server/index.ts
Diff
commit b3c77b475523bf00b477fbcec3b5345bd2d27578
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Wed May 6 17:22:11 2026 -0700
iter 97+98+99+100: PDF export + reader-search + IG share-card + nav surfacing β /issue.pdf?cat= renders /issue or /issue/:cat via Playwright headless chromium with httpCredentials auth, prints Letter format with 0.5/0.4in margins, attachment download w/ dated filename; /search.html standalone reader-facing search w/ Cormorant 54px italic 'Search the issue' masthead + vertical nav + 24px italic input + highlight-matched substrings + 180-char snippet + per-card category + sponsor-candidate marker; /share/:id 1080Γ1920 9:16 IG-story share-card w/ category-gradient bg + 140px italic Cormorant headline + 42px subhead + 48px pull-quote + biz/address/city footer + The Corridor mast bottom-right (auto-scales to viewport for preview, full-size for screenshot/print); /magazine/:id reader toolbar gets 'π± IG share-card' link; /magazine.html nav grows 'π΅ rate card' + 'π search' shortcuts
---
public/magazine.html | 2 +
public/search.html | 106 ++++++++++++++++++++++++++++++++++++++++++++++
src/server/index.ts | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 225 insertions(+)
diff --git a/public/magazine.html b/public/magazine.html
index 6540ff5..ae9e25b 100644
--- a/public/magazine.html
+++ b/public/magazine.html
@@ -229,6 +229,8 @@
<a href="/magazine.html" style="color:var(--metal-glow);font-weight:500">magazine</a>
<a href="/issue" style="color:var(--accent)">β
public issue</a>
<a href="/scoreboard.html">π scoreboard</a>
+ <a href="/search.html">π search</a>
+ <a href="/rate-card.html">π΅ rate card</a>
<a href="/crawl-derby.html">π derby</a>
</nav>
diff --git a/public/search.html b/public/search.html
new file mode 100644
index 0000000..ff311d2
--- /dev/null
+++ b/public/search.html
@@ -0,0 +1,106 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Search Β· 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;1,400;1,500&family=Inter:wght@300;400;500;600&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}
+header{text-align:center;padding:48px 24px 24px;border-bottom:1px solid var(--rule)}
+header .kicker{font-size:9px;letter-spacing:.4em;text-transform:uppercase;color:var(--metal);font-weight:500}
+header h1{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:54px;margin:8px 0 0;color:var(--ink)}
+header h1 em{color:var(--metal);font-style:normal}
+.searchbar{padding:24px;border-bottom:1px solid var(--rule);text-align:center}
+.searchbar input{font-family:'Cormorant Garamond',serif;font-size:24px;font-style:italic;background:transparent;border:none;border-bottom:1px solid var(--rule);color:var(--ink);padding:12px 8px;min-width:480px;outline:none}
+.searchbar input:focus{border-bottom-color:var(--metal)}
+.searchbar .meta{font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-mute);margin-top:14px}
+nav{padding:12px 24px;text-align:center;border-bottom:1px solid var(--rule)}
+nav a{font-size:9px;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-mute);margin:0 10px;text-decoration:none}
+nav a:hover{color:var(--metal)}
+main{max-width:780px;margin:0 auto;padding:32px 24px 80px}
+.result{padding:18px 0;border-bottom:1px solid var(--rule);text-decoration:none;display:block;color:var(--ink)}
+.result h3{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:24px;margin:0 0 4px;color:var(--ink)}
+.result h3 em{font-weight:600;font-style:normal;background:rgba(184,153,104,0.18)}
+.result .biz{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:14px;color:var(--ink-mute);margin-bottom:8px}
+.result .snippet{font-family:'Cormorant Garamond',serif;font-size:14px;line-height:1.5;color:var(--ink)}
+.result .snippet em{font-weight:600;font-style:normal;background:rgba(184,153,104,0.18)}
+.result .meta{margin-top:6px;font-size:9px;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-mute);font-family:'Inter',sans-serif}
+.result:hover{background:rgba(184,153,104,0.04);padding-left:8px;padding-right:8px}
+.empty{padding:80px 24px;text-align:center;color:var(--ink-mute);font-style:italic;font-family:'Cormorant Garamond',serif;font-size:24px}
+</style>
+</head>
+<body>
+<header>
+ <div class="kicker">The Corridor Β· Volume I</div>
+ <h1>Search the <em>issue</em></h1>
+</header>
+<nav>
+ <a href="/issue">All features</a>
+ <a href="/issue/restaurant">Restaurants</a>
+ <a href="/issue/professional">Professional</a>
+ <a href="/issue/medical">Medical</a>
+ <a href="/issue/beauty">Beauty</a>
+ <a href="/issue/salon">Salon</a>
+ <a href="/issue/shop">Shops</a>
+ <a href="/issue/real-estate">Real estate</a>
+ <a href="/issue/hospitality">Hospitality</a>
+ <a href="/issue/fitness">Fitness</a>
+</nav>
+<div class="searchbar">
+ <input id="q" type="search" autofocus placeholder="who's on the corridor?" oninput="onSearch()">
+ <div class="meta" id="count">type a name, vertical, or streetβ¦</div>
+</div>
+<main id="results"></main>
+
+<script>
+function escHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
+function highlight(text, q) {
+ if (!q || !text) return escHtml(text);
+ const safe = escHtml(text);
+ const safeQ = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ return safe.replace(new RegExp(safeQ, 'gi'), m => `<em>${m}</em>`);
+}
+function snippet(text, q, len = 180) {
+ if (!text) return '';
+ if (!q) return text.slice(0, len) + (text.length > len ? 'β¦' : '');
+ const i = text.toLowerCase().indexOf(q.toLowerCase());
+ if (i < 0) return text.slice(0, len) + 'β¦';
+ const start = Math.max(0, i - 60);
+ const end = Math.min(text.length, i + q.length + 120);
+ return (start > 0 ? 'β¦' : '') + text.slice(start, end) + (end < text.length ? 'β¦' : '');
+}
+let _t;
+async function onSearch() {
+ clearTimeout(_t);
+ _t = setTimeout(go, 200);
+}
+async function go() {
+ const q = document.getElementById('q').value.trim();
+ if (!q) {
+ document.getElementById('results').innerHTML = '';
+ document.getElementById('count').textContent = 'type a name, vertical, or streetβ¦';
+ return;
+ }
+ const data = await fetch('/api/magazine?status=published&limit=200').then(r => r.json());
+ const ql = q.toLowerCase();
+ const hits = (data.rows || []).filter(r => {
+ const hay = ((r.name || '') + ' ' + (r.headline || '') + ' ' + (r.subhead || '') + ' ' + (r.editorial || '') + ' ' + (r.address || '') + ' ' + (r.city || '') + ' ' + (r.naics || '') + ' ' + (r.category_tag || '')).toLowerCase();
+ return hay.includes(ql);
+ });
+ document.getElementById('count').innerHTML = `<b style="color:var(--metal-glow);font-family:'Cormorant Garamond',serif;font-style:italic;font-size:18px">${hits.length}</b> ${hits.length === 1 ? 'feature' : 'features'} match`;
+ document.getElementById('results').innerHTML = hits.length === 0
+ ? `<div class="empty">No matches in this issue.</div>`
+ : hits.map(r => `
+ <a class="result" href="/magazine/${r.id}">
+ <h3>${highlight(r.headline || r.name, q)}</h3>
+ <div class="biz">${highlight(r.name, q)}${r.address ? ' Β· ' + highlight(r.address.split(',')[0], q) : ''}</div>
+ <div class="snippet">${highlight(snippet(r.editorial || r.subhead, q), q)}</div>
+ <div class="meta">${escHtml(r.category_tag || '')}${r.paid_ads_count > 0 ? ' Β· $ sponsor candidate' : ''}</div>
+ </a>`).join('');
+}
+</script>
+</body>
+</html>
diff --git a/src/server/index.ts b/src/server/index.ts
index c732260..bc2fcd2 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -73,7 +73,10 @@ const ADMIN_PATHS = [
/^\/magazine(\.html)?\/?$/i,
/^\/api\/magazine(\/.*)?$/i,
/^\/magazine\/\d+\/?$/i,
+ /^\/share\/\d+\/?$/i,
/^\/issue(\/[\w-]+)?\/?$/i,
+ /^\/issue\.pdf\/?$/i,
+ /^\/search(\.html)?\/?$/i,
/^\/scoreboard(\.html)?\/?$/i,
/^\/rate-card(\.html)?\/?$/i,
/^\/sponsor(\.html)?\/?$/i,
@@ -2012,6 +2015,7 @@ article .biz small{display:block;font-family:monospace;font-size:9px;color:var(-
</style></head><body>
<div class="toolbar">
<a href="/magazine.html">β Admin</a>
+ <a href="/issue.pdf${cat ? '?cat=' + encodeURIComponent(cat) : ''}" download>β¬ PDF</a>
<a href="javascript:window.print()">π¨ Print</a>
</div>
<header>
@@ -2043,6 +2047,29 @@ rows.map((f: any) => `
</body></html>`);
});
+// PDF export β render /issue (or /issue/:cat) via Playwright headless and stream to disk
+app.get('/issue.pdf', async (req, res) => {
+ try {
+ const { chromium } = await import('playwright');
+ const cat = String(req.query.cat || '');
+ const url = cat ? `http://127.0.0.1:${PORT}/issue/${encodeURIComponent(cat)}` : `http://127.0.0.1:${PORT}/issue`;
+ const browser = await chromium.launch();
+ const ctx = await browser.newContext({
+ httpCredentials: { username: ADMIN_USER, password: ADMIN_PASS }
+ });
+ const page = await ctx.newPage();
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 });
+ const pdf = await page.pdf({ format: 'Letter', printBackground: true, margin: { top: '0.5in', bottom: '0.5in', left: '0.4in', right: '0.4in' } });
+ await browser.close();
+ const stamp = new Date().toISOString().slice(0, 10);
+ res.setHeader('Content-Type', 'application/pdf');
+ res.setHeader('Content-Disposition', `attachment; filename="the-corridor-${cat || 'issue'}-${stamp}.pdf"`);
+ res.send(pdf);
+ } catch (e: any) {
+ res.status(500).send('PDF render failed: ' + e.message);
+ }
+});
+
// Bulk-publish-all-reviewed: single-click curation finalization
app.post('/api/magazine/publish-reviewed', express.json(), async (req, res) => {
try {
@@ -2075,6 +2102,95 @@ app.post('/api/magazine/review-drafts', express.json(), async (req, res) => {
}
});
+// 9:16 share-card β clean Instagram-story format, screenshot or screencap-ready
+app.get('/share/:id', async (req, res) => {
+ const id = parseInt(req.params.id, 10);
+ if (!Number.isFinite(id)) return res.status(400).send('bad id');
+ const r = await query(
+ `SELECT mf.*, b.name AS biz_name, b.address AS biz_address, b.city
+ FROM magazine_features mf JOIN businesses b ON b.id = mf.business_id
+ WHERE mf.id = $1`,
+ [id]
+ );
+ if (r.rowCount === 0) return res.status(404).send('not found');
+ const f = r.rows[0];
+ const esc = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]!));
+ const cat = f.category_tag || 'shop';
+ const catGradient: Record<string,string> = {
+ restaurant: 'linear-gradient(135deg,#8a3a1a 0%,#c47b5a 60%,#f4d6b8 100%)',
+ professional: 'linear-gradient(135deg,#2d3a4a 0%,#5a6c7d 50%,#8a9ba8 100%)',
+ medical: 'linear-gradient(135deg,#2a5a4a 0%,#5a8a7a 50%,#b0d4c4 100%)',
+ beauty: 'linear-gradient(135deg,#6a3a5a 0%,#a76a8a 50%,#e8c4d4 100%)',
+ salon: 'linear-gradient(135deg,#5a2a4a 0%,#8a5a7a 50%,#d4a4c4 100%)',
+ fitness: 'linear-gradient(135deg,#2a3a1a 0%,#5a7a3a 50%,#a4c47a 100%)',
+ shop: 'linear-gradient(135deg,#4a2a1a 0%,#8a5a3a 50%,#d4a48a 100%)',
+ 'real-estate':'linear-gradient(135deg,#1a3a4a 0%,#3a6a8a 50%,#8aa4c4 100%)',
+ hospitality: 'linear-gradient(135deg,#4a3a1a 0%,#8a6a3a 50%,#d4b48a 100%)',
+ automotive: 'linear-gradient(135deg,#2a2a2a 0%,#5a5a5a 50%,#a4a4a4 100%)',
+ };
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
+ res.send(`<!doctype html><html><head><meta charset="utf-8">
+<title>${esc(f.headline || f.biz_name)} β share card</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,400;0,500;0,600;1,400;1,500&family=Inter:wght@300;400;500;600&display=swap');
+*{margin:0;padding:0;box-sizing:border-box}
+html,body{background:#0a0a0c;font-family:'Inter',sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px}
+.story{
+ width:1080px;height:1920px;
+ /* Scale to viewport */
+ transform-origin:top center;
+ background:${catGradient[cat] || catGradient.shop};
+ position:relative;color:white;padding:96px 96px 120px;display:flex;flex-direction:column;justify-content:space-between;
+ box-shadow:0 30px 80px rgba(0,0,0,0.6);
+}
+@media(max-width:1100px){.story{transform:scale(0.45);height:864px;width:486px;}}
+@media(max-width:600px){.story{transform:scale(0.32);height:614px;width:346px;}}
+.story::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at top,rgba(255,255,255,0.08),transparent 60%),linear-gradient(180deg,transparent 50%,rgba(0,0,0,0.5))}
+.story::after{content:'';position:absolute;inset:48px;border:2px solid rgba(255,255,255,0.18);pointer-events:none}
+.story > *{position:relative;z-index:2}
+.kicker{font-size:24px;letter-spacing:.5em;text-transform:uppercase;font-weight:600;color:rgba(255,255,255,0.9);margin-bottom:20px}
+.cat{display:inline-block;background:rgba(0,0,0,0.4);color:white;padding:14px 28px;font-size:22px;letter-spacing:.4em;text-transform:uppercase;font-weight:600;margin-bottom:48px}
+.headline{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:140px;line-height:0.92;letter-spacing:-0.02em;margin-bottom:32px;text-shadow:0 4px 20px rgba(0,0,0,0.4)}
+.headline em{font-style:normal;color:rgba(255,255,255,0.75)}
+.subhead{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:300;font-size:42px;line-height:1.3;color:rgba(255,255,255,0.85);max-width:840px;margin-bottom:auto}
+.pull{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:48px;line-height:1.3;border-left:4px solid white;padding:16px 0 16px 32px;margin-top:48px;max-width:840px}
+.footer{display:flex;justify-content:space-between;align-items:flex-end;margin-top:48px}
+.footer .biz{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:42px;line-height:1.2}
+.footer .biz small{display:block;font-family:'Inter',sans-serif;font-style:normal;font-size:18px;font-weight:400;letter-spacing:.05em;opacity:0.85;margin-top:6px}
+.footer .brand{text-align:right}
+.footer .brand .mast{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:64px;line-height:1}
+.footer .brand .mast em{font-style:normal;opacity:0.7}
+.footer .brand .vol{font-size:16px;letter-spacing:.4em;text-transform:uppercase;font-weight:600;margin-top:8px;opacity:0.85}
+.toolbar{position:fixed;top:14px;right:14px;display:flex;gap:6px;z-index:100}
+.toolbar a{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:#888;background:#1a1a1a;padding:6px 12px;border:1px solid #333;text-decoration:none;font-family:'Inter',sans-serif}
+.toolbar a:hover{color:white;border-color:white}
+@media print{.toolbar{display:none}body{padding:0;background:white}.story{transform:none;box-shadow:none}}
+</style>
+</head><body>
+<div class="toolbar">
+ <a href="/magazine/${id}">β Reader</a>
+ <a href="javascript:window.print()">π¨ Print</a>
+</div>
+<div class="story">
+ <div>
+ <div class="kicker">The Corridor</div>
+ <span class="cat">${esc(cat.replace(/-/g,' '))}</span>
+ <h1 class="headline">${esc(f.headline || f.biz_name)}</h1>
+ <div class="subhead">${esc(f.subhead || '')}</div>
+ </div>
+ ${f.pull_quote ? `<div class="pull">"${esc(f.pull_quote)}"</div>` : ''}
+ <div class="footer">
+ <div class="biz">${esc(f.biz_name)}<small>${esc(f.biz_address || '')}${f.city ? ' Β· ' + esc(f.city) : ''}</small></div>
+ <div class="brand">
+ <div class="mast">The <em>Corridor</em></div>
+ <div class="vol">Vol I Β· 2026</div>
+ </div>
+ </div>
+</div>
+</body></html>`);
+});
+
// Full-page reader for one feature
app.get('/magazine/:id', async (req, res) => {
const id = parseInt(req.params.id, 10);
@@ -2135,6 +2251,7 @@ footer{margin-top:48px;padding:24px 0;text-align:center;font-size:10px;letter-sp
<div class="toolbar">
<div class="breadcrumb"><a href="/magazine.html">β The Corridor</a> Β· ${esc(f.category_tag || 'feature')}</div>
<div>
+ <a href="/share/${id}" target="_blank">π± IG share-card</a>
<a href="javascript:window.print()">π¨ Print</a>
<a href="/pitches.html?id=${f.business_id}" target="_blank">Pipeline β</a>
</div>
β 07a36d7 iter 94+95+96: sponsorship rate-card + author byline + month
Β·
back to Ventura Corridor
Β·
iter 101: /sponsor/:id owner-claim form β public-facing per- 18a9a29 β