← back to Ventura Corridor
iter 104+105+106+107+108: regen-thin job + sitemap + robots + random + RSS + friendly 404 — src/jobs/regen_thin.ts calls /api/magazine/:id/regen for any feature with editorial<200 chars (default 50/run); /sitemap.xml lists /issue + per-vertical /issue/:cat + every published /magazine/:id with lastmod; /robots.txt blocks all crawlers (loopback build); /api/magazine/random?cat= returns one published feature for fleet-tv-style rotators; /issue/feed.xml + /issue/:cat/feed.xml RSS 2.0 with content:encoded full editorial; magazine-themed 404 ('Not in this issue') with Cormorant 84px italic + suggested nav (issue/search/scoreboard/words/rates)
fa94a30bdb3e26f7165d1e02091daa24f81f933d · 2026-05-06 17:37:32 -0700 · SteveStudio2
Files touched
M package.jsonA src/jobs/regen_thin.tsM src/server/index.ts
Diff
commit fa94a30bdb3e26f7165d1e02091daa24f81f933d
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Wed May 6 17:37:32 2026 -0700
iter 104+105+106+107+108: regen-thin job + sitemap + robots + random + RSS + friendly 404 — src/jobs/regen_thin.ts calls /api/magazine/:id/regen for any feature with editorial<200 chars (default 50/run); /sitemap.xml lists /issue + per-vertical /issue/:cat + every published /magazine/:id with lastmod; /robots.txt blocks all crawlers (loopback build); /api/magazine/random?cat= returns one published feature for fleet-tv-style rotators; /issue/feed.xml + /issue/:cat/feed.xml RSS 2.0 with content:encoded full editorial; magazine-themed 404 ('Not in this issue') with Cormorant 84px italic + suggested nav (issue/search/scoreboard/words/rates)
---
package.json | 3 +-
src/jobs/regen_thin.ts | 45 ++++++++++++++
src/server/index.ts | 155 +++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 202 insertions(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 4b74d38..530e840 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,8 @@
"magazine:watchdog": "tsx src/jobs/magazine_watchdog.ts",
"magazine:normalize": "tsx src/jobs/normalize_categories.ts",
"magazine:morning-email": "tsx src/jobs/morning_feature_email.ts",
- "magazine:auto-publish": "tsx src/jobs/auto_publish_top.ts"
+ "magazine:auto-publish": "tsx src/jobs/auto_publish_top.ts",
+ "magazine:regen-thin": "tsx src/jobs/regen_thin.ts"
},
"dependencies": {
"dotenv": "^16.4.5",
diff --git a/src/jobs/regen_thin.ts b/src/jobs/regen_thin.ts
new file mode 100644
index 0000000..1d0ae64
--- /dev/null
+++ b/src/jobs/regen_thin.ts
@@ -0,0 +1,45 @@
+/**
+ * Identify magazine_features with thin editorial (<MIN_LEN chars) and
+ * call /api/magazine/:id/regen to refresh them via Mac1 Ollama.
+ *
+ * $ npx tsx src/jobs/regen_thin.ts # rescue all thin features
+ * $ npx tsx src/jobs/regen_thin.ts 20 # max 20
+ */
+import 'dotenv/config';
+import { pool, query } from '../../db/pool.ts';
+
+const N_MAX = parseInt(process.argv.find(a => /^\d+$/.test(a)) || '50', 10);
+const MIN_LEN = parseInt(process.env.MIN_LEN || '200', 10);
+const SERVER = process.env.VC_URL || 'http://127.0.0.1:9780';
+const AUTH = 'Basic ' + Buffer.from('admin:DWSecure2024!').toString('base64');
+
+async function main() {
+ const r = await query(
+ `SELECT id, length(editorial) AS len, headline FROM magazine_features
+ WHERE editorial IS NULL OR length(editorial) < $1
+ ORDER BY generated_at ASC
+ LIMIT $2`,
+ [MIN_LEN, N_MAX]
+ );
+ console.log(`[regen_thin] ${r.rowCount} features below ${MIN_LEN} chars to refresh`);
+ let ok = 0, fail = 0;
+ for (const row of r.rows) {
+ try {
+ const t0 = Date.now();
+ const resp = await fetch(`${SERVER}/api/magazine/${row.id}/regen`, {
+ method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: AUTH }
+ });
+ if (!resp.ok) throw new Error(`${resp.status}: ${(await resp.text()).slice(0, 100)}`);
+ const j = await resp.json();
+ console.log(` ✓ ${row.id} (${row.len || 0}ch → fresh, ${Date.now() - t0}ms) "${(j.headline || '').slice(0, 50)}"`);
+ ok++;
+ } catch (e: any) {
+ console.log(` ✕ ${row.id} (${row.len || 0}ch) — ${e.message.slice(0, 80)}`);
+ fail++;
+ }
+ }
+ console.log(`[regen_thin] ok=${ok} fail=${fail}`);
+ await pool.end();
+}
+
+main().catch(e => { console.error(e); process.exit(1); });
diff --git a/src/server/index.ts b/src/server/index.ts
index adcadff..5670946 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -78,6 +78,9 @@ const ADMIN_PATHS = [
/^\/issue\.pdf\/?$/i,
/^\/search(\.html)?\/?$/i,
/^\/words\/?$/i,
+ /^\/sitemap\.xml\/?$/i,
+ /^\/robots\.txt\/?$/i,
+ /^\/issue(\/[\w-]+)?\/feed\.xml\/?$/i,
/^\/scoreboard(\.html)?\/?$/i,
/^\/rate-card(\.html)?\/?$/i,
/^\/sponsor(\.html)?\/?$/i,
@@ -1793,6 +1796,28 @@ app.get('/api/magazine/feature-of-the-day', async (_req, res) => {
}
});
+// Random published feature — for fleet-tv-style rotators or sidebar widgets
+app.get('/api/magazine/random', async (req, res) => {
+ try {
+ const cat = String(req.query.cat || '');
+ const where: string[] = [`status = 'published'`, `length(editorial) > 100`];
+ const params: any[] = [];
+ if (cat) { params.push(cat); where.push(`category_tag = $${params.length}`); }
+ const r = await query(
+ `SELECT mf.id, mf.headline, mf.subhead, mf.editorial, mf.pull_quote, mf.category_tag,
+ 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 ${where.join(' AND ')}
+ ORDER BY random() LIMIT 1`,
+ params
+ );
+ if (r.rowCount === 0) return res.json({ feature: null });
+ res.json({ feature: r.rows[0], permalink: `/magazine/${r.rows[0].id}` });
+ } catch (e: any) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
app.get('/api/magazine/scoreboard', async (_req, res) => {
try {
const [topViewed, hotVerticals, latest, longest, sponsors] = await Promise.all([
@@ -2050,6 +2075,98 @@ rows.map((f: any) => `
</body></html>`);
});
+// /sitemap.xml — every published feature + every per-vertical issue page
+app.get('/sitemap.xml', async (_req, res) => {
+ try {
+ const baseUrl = process.env.PUBLIC_BASE_URL || 'http://127.0.0.1:9780';
+ const r = await query(`
+ SELECT id, COALESCE(published_at, generated_at) AS ts
+ FROM magazine_features
+ WHERE status = 'published'
+ ORDER BY published_at DESC
+ `);
+ const cats = await query(`
+ SELECT DISTINCT category_tag FROM magazine_features WHERE status='published' AND category_tag IS NOT NULL
+ `);
+ const fmt = (d: any) => new Date(d).toISOString();
+ const urls: string[] = [
+ `<url><loc>${baseUrl}/issue</loc><changefreq>daily</changefreq><priority>1.0</priority></url>`,
+ `<url><loc>${baseUrl}/scoreboard.html</loc><changefreq>daily</changefreq><priority>0.6</priority></url>`,
+ `<url><loc>${baseUrl}/words</loc><changefreq>weekly</changefreq><priority>0.5</priority></url>`,
+ `<url><loc>${baseUrl}/rate-card.html</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>`,
+ ];
+ for (const c of cats.rows) {
+ urls.push(`<url><loc>${baseUrl}/issue/${c.category_tag}</loc><changefreq>weekly</changefreq><priority>0.8</priority></url>`);
+ }
+ for (const f of r.rows) {
+ urls.push(`<url><loc>${baseUrl}/magazine/${f.id}</loc><lastmod>${fmt(f.ts)}</lastmod><changefreq>monthly</changefreq><priority>0.6</priority></url>`);
+ }
+ res.setHeader('Content-Type', 'application/xml; charset=utf-8');
+ res.send(`<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+${urls.join('\n')}
+</urlset>`);
+ } catch (e: any) {
+ res.status(500).send('error: ' + e.message);
+ }
+});
+
+// /issue/feed.xml or /issue/:cat/feed.xml — RSS for the (vertical-) issue
+app.get(['/issue/feed.xml', '/issue/:cat/feed.xml'], async (req, res) => {
+ try {
+ const cat = (req.params as any).cat || null;
+ const baseUrl = process.env.PUBLIC_BASE_URL || 'http://127.0.0.1:9780';
+ const where: string[] = [`status = 'published'`];
+ const params: any[] = [];
+ if (cat) { params.push(cat); where.push(`category_tag = $${params.length}`); }
+ const r = await query(
+ `SELECT mf.id, mf.headline, mf.subhead, mf.editorial, mf.published_at, mf.category_tag,
+ b.name AS biz_name
+ FROM magazine_features mf JOIN businesses b ON b.id = mf.business_id
+ WHERE ${where.join(' AND ')}
+ ORDER BY published_at DESC LIMIT 50`,
+ params
+ );
+ const esc = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]!));
+ const items = r.rows.map((f: any) => `
+ <item>
+ <title>${esc(f.headline || f.biz_name)}</title>
+ <link>${baseUrl}/magazine/${f.id}</link>
+ <guid isPermaLink="true">${baseUrl}/magazine/${f.id}</guid>
+ <pubDate>${new Date(f.published_at || Date.now()).toUTCString()}</pubDate>
+ <category>${esc(f.category_tag || 'feature')}</category>
+ <description>${esc(f.subhead || '')} — ${esc(f.biz_name)}</description>
+ <content:encoded><![CDATA[${esc(f.editorial || '')}]]></content:encoded>
+ </item>`).join('');
+ const title = cat ? `The Corridor — ${cat}` : 'The Corridor';
+ res.setHeader('Content-Type', 'application/rss+xml; charset=utf-8');
+ res.send(`<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
+ <channel>
+ <title>${esc(title)}</title>
+ <link>${baseUrl}${cat ? '/issue/' + esc(cat) : '/issue'}</link>
+ <description>A magazine of who's here on Ventura Boulevard.${cat ? ' Vertical: ' + esc(cat) + '.' : ''}</description>
+ <language>en-us</language>
+ <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>${items}
+ </channel>
+</rss>`);
+ } catch (e: any) {
+ res.status(500).send('error: ' + e.message);
+ }
+});
+
+app.get('/robots.txt', (_req, res) => {
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
+ // Loopback-only: discourage crawl entirely until Steve goes public.
+ res.send(`User-agent: *
+Disallow: /
+
+# The Corridor magazine — local development build.
+# When this site goes public, swap Disallow back to Allow and uncomment Sitemap.
+# Sitemap: /sitemap.xml
+`);
+});
+
// /words — what words does The Corridor desk reach for most? frequency analysis
// of all editorial bodies, minus a stop-word list. Renders a typographic word
// cloud sized by frequency.
@@ -3023,6 +3140,44 @@ app.use('/screenshots', express.static(path.resolve(__dirname, '../../data/scree
// ─── Static viewer ─────────────────────────────────────────────────────
app.use('/', express.static(path.resolve(__dirname, '../../public')));
+// Friendly 404 — magazine-flavored, suggests likely routes
+app.use((req, res) => {
+ if (req.accepts('html')) {
+ res.status(404).type('html').send(`<!doctype html><html><head><meta charset="utf-8">
+<title>Not in this issue · The Corridor</title>
+<style>
+@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;1,400;1,500&family=Inter:wght@300;400;500&display=swap');
+*{margin:0;padding:0;box-sizing:border-box}
+html,body{background:#faf6ee;color:#1a1815;font-family:'Inter',sans-serif;font-weight:300;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
+.card{max-width:520px;text-align:center}
+.kicker{font-size:9px;letter-spacing:.4em;text-transform:uppercase;color:#8a6d3b;font-weight:500}
+h1{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:84px;line-height:.95;margin:14px 0 12px;letter-spacing:-0.02em}
+h1 em{font-style:normal;color:#8a6d3b}
+.deck{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:18px;color:#6e6356;line-height:1.4;margin-bottom:32px}
+.path{font-family:'JetBrains Mono',monospace;font-size:12px;color:#888;background:rgba(184,153,104,0.08);padding:6px 12px;display:inline-block;margin-bottom:24px}
+nav{display:flex;gap:14px;justify-content:center;flex-wrap:wrap;margin-top:16px}
+nav a{font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:#8a6d3b;text-decoration:none;border:1px solid #d8cdb8;padding:8px 16px}
+nav a:hover{color:#6a3a1a;border-color:#6a3a1a}
+</style></head><body>
+<div class="card">
+ <div class="kicker">The Corridor · 404</div>
+ <h1>Not in <em>this issue</em>.</h1>
+ <div class="deck">The page you were looking for didn't make the cut. Try one of these.</div>
+ <div class="path">${req.path.slice(0, 80)}</div>
+ <nav>
+ <a href="/issue">Read the issue</a>
+ <a href="/search.html">Search</a>
+ <a href="/scoreboard.html">Scoreboard</a>
+ <a href="/words">Words</a>
+ <a href="/rate-card.html">Rates</a>
+ </nav>
+</div>
+</body></html>`);
+ } else {
+ res.status(404).type('json').send({ error: 'not found', path: req.path });
+ }
+});
+
// 127.0.0.1 only — directory data stays local per the lawyer/doctor precedent.
app.listen(PORT, '127.0.0.1', () => {
console.log(`[ventura-corridor] listening on http://127.0.0.1:${PORT}`);
← 3bb691b iter 102+103: nightly auto-publish + /words page — src/jobs/
·
back to Ventura Corridor
·
fix: move RSS route ahead of /issue/:cat handler — Express w a45aad1 →