โ back to Ventura Corridor
iter 121+122: og+JSON-LD on per-vertical /issue/:cat + per-feature TTS audio โ /issue/:cat now emits og:type/title/description/url/site_name + twitter:card summary_large_image (image=first feature's /share/:id) + JSON-LD CollectionPage with hasPart NewsArticle list (top 30) + RSS alternate link + JSON Feed alternate link, so per-vertical pages share-render with proper social cards. /api/magazine/:id/audio.aiff renders TTS via macOS 'say -v Samantha -r 180' from headline+subhead+editorial+pull-quote, caches by editorial-hash to data/magazine-audio/, 60s timeout, 24h browser cache; /magazine/:id reader toolbar gets ๐ Listen link
bd681e7c52cc47611c81c7e356d664570c6403d0 ยท 2026-05-06 18:20:14 -0700 ยท SteveStudio2
Files touched
A data/magazine-audio/1-6e3ce8d703a7.aiffM src/server/index.ts
Diff
commit bd681e7c52cc47611c81c7e356d664570c6403d0
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Wed May 6 18:20:14 2026 -0700
iter 121+122: og+JSON-LD on per-vertical /issue/:cat + per-feature TTS audio โ /issue/:cat now emits og:type/title/description/url/site_name + twitter:card summary_large_image (image=first feature's /share/:id) + JSON-LD CollectionPage with hasPart NewsArticle list (top 30) + RSS alternate link + JSON Feed alternate link, so per-vertical pages share-render with proper social cards. /api/magazine/:id/audio.aiff renders TTS via macOS 'say -v Samantha -r 180' from headline+subhead+editorial+pull-quote, caches by editorial-hash to data/magazine-audio/, 60s timeout, 24h browser cache; /magazine/:id reader toolbar gets ๐ Listen link
---
data/magazine-audio/1-6e3ce8d703a7.aiff | Bin 0 -> 2150464 bytes
src/server/index.ts | 87 ++++++++++++++++++++++++++++++++
2 files changed, 87 insertions(+)
diff --git a/data/magazine-audio/1-6e3ce8d703a7.aiff b/data/magazine-audio/1-6e3ce8d703a7.aiff
new file mode 100644
index 0000000..2c3b79b
Binary files /dev/null and b/data/magazine-audio/1-6e3ce8d703a7.aiff differ
diff --git a/src/server/index.ts b/src/server/index.ts
index f9d5f7d..f898742 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1841,6 +1841,59 @@ app.get('/api/magazine/random', async (req, res) => {
}
});
+// Per-feature TTS audio โ /api/magazine/:id/audio.aiff
+// Uses macOS `say` command. Caches by editorial-hash so we don't re-render every visit.
+import { existsSync as _fs_exists, mkdirSync as _fs_mkdir, statSync as _fs_stat, createReadStream as _fs_stream } from 'node:fs';
+import { spawnSync as _spawn_sync } from 'node:child_process';
+import { createHash as _crypto_hash } from 'node:crypto';
+
+app.get('/api/magazine/:id/audio.aiff', async (req, res) => {
+ try {
+ const id = parseInt(req.params.id, 10);
+ if (!Number.isFinite(id)) return res.status(400).send('bad id');
+ const r = await query(
+ `SELECT mf.headline, mf.subhead, mf.editorial, mf.pull_quote, b.name AS biz_name
+ 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 text = [
+ 'From The Corridor.',
+ f.headline || f.biz_name,
+ f.subhead || '',
+ f.editorial || '',
+ f.pull_quote ? `Quote: ${f.pull_quote}` : '',
+ `End of feature.`
+ ].filter(Boolean).join('\n\n');
+
+ const audioDir = path.resolve(__dirname, '../../data/magazine-audio');
+ if (!_fs_exists(audioDir)) _fs_mkdir(audioDir, { recursive: true });
+ const hash = _crypto_hash('sha1').update(text).digest('hex').slice(0, 12);
+ const filename = `${id}-${hash}.aiff`;
+ const filepath = path.join(audioDir, filename);
+
+ if (!_fs_exists(filepath)) {
+ // Use a Steve-friendly voice. macOS default voices: Samantha, Karen, Daniel, Alex.
+ const voice = process.env.SAY_VOICE || 'Samantha';
+ const result = _spawn_sync('say', ['-v', voice, '-r', '180', '-o', filepath, text], {
+ timeout: 60_000
+ });
+ if (result.status !== 0 || !_fs_exists(filepath)) {
+ return res.status(500).send('say failed: ' + (result.stderr?.toString() || ''));
+ }
+ }
+ const stat = _fs_stat(filepath);
+ res.setHeader('Content-Type', 'audio/aiff');
+ res.setHeader('Content-Length', String(stat.size));
+ res.setHeader('Cache-Control', 'public, max-age=86400');
+ _fs_stream(filepath).pipe(res);
+ } catch (e: any) {
+ res.status(500).send('error: ' + e.message);
+ }
+});
+
// Outbound tap counter โ /api/magazine/:id/tap?to=sponsor|share|website|other
app.get('/api/magazine/:id/tap', async (req, res) => {
try {
@@ -2196,10 +2249,43 @@ app.get(['/issue', '/issue/:cat'], async (req, res) => {
);
const esc = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]!));
const rows = r.rows;
+ const baseUrl = process.env.PUBLIC_BASE_URL || 'http://127.0.0.1:9780';
+ const issueTitle = monthArchive ? `Archive ยท ${monthArchive}` : (cat ? `${cat} on The Corridor` : `The Corridor ยท Volume I`);
+ const issueDesc = cat
+ ? `${rows.length} ${cat} feature${rows.length === 1 ? '' : 's'} on Ventura Boulevard.`
+ : `${rows.length} stories from Ventura Boulevard's restaurants, professionals, and ateliers.`;
+ const issueUrl = `${baseUrl}/issue${cat ? '/' + cat : (monthArchive ? '/' + monthArchive : '')}`;
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.send(`<!doctype html><html lang="en"><head><meta charset="utf-8">
<title>The Corridor ยท ${monthArchive ? esc(monthArchive) + ' archive' : 'Issue'}${cat ? ' ยท ' + esc(cat) : ''}</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
+<meta name="description" content="${esc(issueDesc)}">
+<meta property="og:type" content="website">
+<meta property="og:title" content="${esc(issueTitle)}">
+<meta property="og:description" content="${esc(issueDesc)}">
+<meta property="og:url" content="${issueUrl}">
+<meta property="og:site_name" content="The Corridor">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="${esc(issueTitle)}">
+<meta name="twitter:description" content="${esc(issueDesc)}">
+${rows[0] ? `<meta property="og:image" content="${baseUrl}/share/${rows[0].id}"><meta name="twitter:image" content="${baseUrl}/share/${rows[0].id}">` : ''}
+${cat ? `<link rel="alternate" type="application/rss+xml" title="${esc(issueTitle)} feed" href="${baseUrl}/issue/${cat}/feed.xml">` : `<link rel="alternate" type="application/rss+xml" title="The Corridor feed" href="${baseUrl}/issue/feed.xml">`}
+<link rel="alternate" type="application/json" title="The Corridor JSON Feed" href="${baseUrl}/api/magazine.json">
+<script type="application/ld+json">${JSON.stringify({
+ '@context': 'https://schema.org',
+ '@type': 'CollectionPage',
+ name: issueTitle,
+ description: issueDesc,
+ url: issueUrl,
+ publisher: { '@type': 'Organization', name: 'The Corridor' },
+ numberOfItems: rows.length,
+ hasPart: rows.slice(0, 30).map((f: any) => ({
+ '@type': 'NewsArticle',
+ headline: f.headline || f.biz_name,
+ url: `${baseUrl}/magazine/${f.id}`,
+ articleSection: f.category_tag,
+ }))
+})}</script>
<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&display=swap');
:root{--paper:#faf6ee;--ink:#1a1815;--ink-mute:#6e6356;--metal:#8a6d3b;--metal-glow:#b89968;--accent:#6a3a1a;--rule:#d8cdb8}
@@ -2984,6 +3070,7 @@ footer{margin-top:48px;padding:24px 0;text-align:center;font-size:10px;letter-sp
<div class="breadcrumb"><a href="/magazine.html">โ The Corridor</a> ยท ${esc(f.category_tag || 'feature')}</div>
<div>
<a href="/api/magazine/${id}/tap?to=share&dest=${encodeURIComponent(`${baseUrl}/share/${id}`)}" target="_blank">๐ฑ IG share-card</a>
+ <a href="/api/magazine/${id}/audio.aiff" target="_blank" title="Listen โ Samantha reads this feature">๐ Listen</a>
<a href="/api/magazine/${id}/tap?to=sponsor&dest=${encodeURIComponent(`${baseUrl}/sponsor/${id}`)}" style="color:var(--accent,#6a3a1a);border-color:var(--accent,#6a3a1a)">โญ Sponsor</a>
<a href="javascript:window.print()">๐จ Print</a>
<a href="/pitches.html?id=${f.business_id}" target="_blank">Pipeline โ</a>
โ 93a2f1a iter 120: public /find โ reader-facing finder by suite/stree
ยท
back to Ventura Corridor
ยท
iter 123+124: AIFFโM4A audio conversion + scoreboard top-tap ead33e9 โ