← back to Ventura Corridor
feat(news): "Latest from their site" section on /magazine/:id
33b09e9ad15db9f15da19ef986865d10459e5ed7 · 2026-05-07 16:34:13 -0700 · SteveStudio2
Closes the loop between editorial features and live scraped news.
The magazine article view (/magazine/:id) now includes a 5-item
news rail above the building-mates and category-related sections,
showing what the business has been actually publishing on their
own website.
Implementation:
- new newsBiz query in the /magazine/:id handler returns last 5
news_items for f.business_id
- rendered between the existing "Pipeline events" details and the
"In the same building" mates section
- safeUrl scheme-validation pattern carried forward from tick 43:
href is escaped + http(s)-only, falling back to "#"
- "All news from <biz> -->" deep-link goes to /business/:id/news
- Hidden when newsBiz.rows is empty so unpublished biz features
don't grow a blank section
Real reach: 3 features currently on news-having biz (Manu Farrarons,
Coffee Bean & Tea Leaf, $300 Data Recovery). Grows automatically
with the nightly scrape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 33b09e9ad15db9f15da19ef986865d10459e5ed7
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Thu May 7 16:34:13 2026 -0700
feat(news): "Latest from their site" section on /magazine/:id
Closes the loop between editorial features and live scraped news.
The magazine article view (/magazine/:id) now includes a 5-item
news rail above the building-mates and category-related sections,
showing what the business has been actually publishing on their
own website.
Implementation:
- new newsBiz query in the /magazine/:id handler returns last 5
news_items for f.business_id
- rendered between the existing "Pipeline events" details and the
"In the same building" mates section
- safeUrl scheme-validation pattern carried forward from tick 43:
href is escaped + http(s)-only, falling back to "#"
- "All news from <biz> -->" deep-link goes to /business/:id/news
- Hidden when newsBiz.rows is empty so unpublished biz features
don't grow a blank section
Real reach: 3 features currently on news-having biz (Manu Farrarons,
Coffee Bean & Tea Leaf, $300 Data Recovery). Grows automatically
with the nightly scrape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
src/server/index.ts | 39 +++++++++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/src/server/index.ts b/src/server/index.ts
index 75e6b45..c205699 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -4384,6 +4384,17 @@ app.get('/magazine/:id', async (req, res) => {
LIMIT 3`,
[f.category_tag, id]
);
+ // Latest 5 scraped news/blog/press articles from this business's website.
+ // Closes the loop between editorial features and live news so readers see
+ // both "what we said about them" + "what they're saying right now".
+ const newsBiz = await query(
+ `SELECT id, source_url, title, excerpt, summary, published_guess, fetched_at
+ FROM news_items WHERE business_id = $1
+ ORDER BY COALESCE(published_guess, fetched_at::date) DESC,
+ fetched_at DESC
+ LIMIT 5`,
+ [f.business_id]
+ );
// Pull last 10 pipeline events for the underlying business (status changes, walks, replies, etc)
const events = await query(
`SELECT pel.event_type, pel.old_value, pel.new_value, pel.occurred_at
@@ -4511,6 +4522,34 @@ footer{margin-top:48px;padding:24px 0;text-align:center;font-size:10px;letter-sp
</div>
<div style="margin-top:8px;font-size:10px;color:var(--ink-mute);font-style:italic">From the DW outreach pipeline that produces the magazine — every walk, every reply, every status change.</div>
</details>` : ''}
+ ${newsBiz.rows.length > 0 ? `<section style="margin-top:48px;padding-top:24px;border-top:1px solid var(--rule)">
+ <div style="display:flex;justify-content:space-between;align-items:baseline;font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:14px">
+ <span>📰 Latest from their site · ${newsBiz.rowCount}</span>
+ <a href="/business/${f.business_id}/news" style="color:var(--metal);text-decoration:none">All news from ${esc(f.biz_name)} ↗</a>
+ </div>
+ <div style="display:flex;flex-direction:column;gap:0">
+ ${newsBiz.rows.map((n: any) => {
+ const dt = n.published_guess || n.fetched_at;
+ const dateStr = dt ? new Date(dt).toLocaleDateString('en-US', { month:'short', day:'numeric', year:'numeric' }) : '';
+ const text = n.summary || n.excerpt || '';
+ let host = '';
+ try { host = new URL(n.source_url).hostname.replace(/^www\./, ''); } catch {}
+ const safeHref = (() => {
+ try {
+ const u = new URL(String(n.source_url || ''));
+ return (u.protocol === 'http:' || u.protocol === 'https:') ? esc(u.toString()) : '#';
+ } catch { return '#'; }
+ })();
+ return `<a href="${safeHref}" target="_blank" rel="noopener noreferrer" style="display:block;padding:12px 0;border-bottom:1px dotted var(--rule);color:var(--ink);text-decoration:none">
+ <h4 style="font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:18px;line-height:1.2;margin:0 0 4px">${esc((n.title || '(untitled)').slice(0, 140))}</h4>
+ <p style="font-size:13px;line-height:1.5;color:var(--ink-mute);margin:0 0 6px">${esc(text.slice(0, 220))}${text.length > 220 ? '…' : ''}</p>
+ <div style="font-family:'Inter',sans-serif;font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-mute);display:flex;gap:10px">
+ <span>${esc(dateStr)}</span><span>·</span><span style="color:var(--metal)">${esc(host)}</span>
+ </div>
+ </a>`;
+ }).join('')}
+ </div>
+ </section>` : ''}
${mates.rows.length > 0 ? `<section style="margin-top:48px;padding-top:24px;border-top:1px solid var(--rule)">
<div style="display:flex;justify-content:space-between;align-items:baseline;font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:14px">
<span>In the same building · ${esc(bldg)}</span>
← ec89429 feat(news): /api/news/by-vertical + heatmap on /coverage.htm
·
back to Ventura Corridor
·
feat(seo): include news pages in /sitemap.xml 9027832 →