← back to Ventura Corridor
iter 115+116+117: og/twitter/JSON-LD meta + /api/magazine.json + tap counter — reader page emits og:type/title/description/url/image (1080×1920 from /share/:id) + twitter:card summary_large_image + JSON-LD NewsArticle schema (author=The Corridor desk, publisher logo, articleSection, LocalBusiness about); /api/magazine.json renders JSON Feed 1.1 with id/url/title/summary/content_text/date_published/tags/image/_corridor for external rotators; magazine_features.taps + taps_breakdown JSONB columns track outbound clicks; /api/magazine/:id/tap?to=&dest= increments taps + redirects 302 to dest if provided; /magazine/:id reader toolbar 📱 share + ⭐ Sponsor links now route through tap counter for analytics
cfd5c5a6715f2dfb9c02e745b7b7f66a01386a5a · 2026-05-06 18:02:37 -0700 · SteveStudio2
Files touched
Diff
commit cfd5c5a6715f2dfb9c02e745b7b7f66a01386a5a
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Wed May 6 18:02:37 2026 -0700
iter 115+116+117: og/twitter/JSON-LD meta + /api/magazine.json + tap counter — reader page emits og:type/title/description/url/image (1080×1920 from /share/:id) + twitter:card summary_large_image + JSON-LD NewsArticle schema (author=The Corridor desk, publisher logo, articleSection, LocalBusiness about); /api/magazine.json renders JSON Feed 1.1 with id/url/title/summary/content_text/date_published/tags/image/_corridor for external rotators; magazine_features.taps + taps_breakdown JSONB columns track outbound clicks; /api/magazine/:id/tap?to=&dest= increments taps + redirects 302 to dest if provided; /magazine/:id reader toolbar 📱 share + ⭐ Sponsor links now route through tap counter for analytics
---
src/server/index.ts | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 96 insertions(+), 2 deletions(-)
diff --git a/src/server/index.ts b/src/server/index.ts
index 32526a0..7f075e8 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1838,6 +1838,71 @@ app.get('/api/magazine/random', async (req, res) => {
}
});
+// Outbound tap counter — /api/magazine/:id/tap?to=sponsor|share|website|other
+app.get('/api/magazine/:id/tap', async (req, res) => {
+ try {
+ const id = parseInt(req.params.id, 10);
+ const to = String(req.query.to || 'other').slice(0, 32).replace(/[^a-z0-9_-]/gi, '');
+ if (!Number.isFinite(id)) return res.status(400).end();
+ await query(
+ `UPDATE magazine_features
+ SET taps = taps + 1,
+ taps_breakdown = jsonb_set(
+ COALESCE(taps_breakdown, '{}'::jsonb),
+ ARRAY[$1::text],
+ to_jsonb(COALESCE((taps_breakdown->>$1)::int, 0) + 1),
+ true
+ )
+ WHERE id = $2`,
+ [to, id]
+ );
+ // Redirect to the dest if provided as ?dest=
+ const dest = String(req.query.dest || '');
+ if (dest && /^https?:\/\//.test(dest)) return res.redirect(302, dest);
+ res.json({ ok: true, to });
+ } catch (e: any) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
+// JSON Feed 1.1 — full feed of published features (https://jsonfeed.org/)
+app.get('/api/magazine.json', async (_req, res) => {
+ try {
+ const baseUrl = process.env.PUBLIC_BASE_URL || 'http://127.0.0.1:9780';
+ const r = await query(
+ `SELECT mf.id, mf.headline, mf.subhead, mf.editorial, mf.pull_quote, mf.category_tag,
+ mf.generated_at, mf.published_at,
+ 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.status = 'published'
+ ORDER BY mf.published_at DESC NULLS LAST, mf.generated_at DESC
+ LIMIT 200`
+ );
+ res.json({
+ version: 'https://jsonfeed.org/version/1.1',
+ title: 'The Corridor',
+ home_page_url: `${baseUrl}/issue`,
+ feed_url: `${baseUrl}/api/magazine.json`,
+ description: "A magazine of who's here on Ventura Boulevard.",
+ authors: [{ name: 'The Corridor desk' }],
+ language: 'en',
+ items: r.rows.map((f: any) => ({
+ id: `${baseUrl}/magazine/${f.id}`,
+ url: `${baseUrl}/magazine/${f.id}`,
+ title: f.headline || f.biz_name,
+ summary: f.subhead || '',
+ content_text: f.editorial || '',
+ date_published: f.published_at ? new Date(f.published_at).toISOString() : new Date(f.generated_at).toISOString(),
+ tags: [f.category_tag].filter(Boolean),
+ image: `${baseUrl}/share/${f.id}`,
+ _corridor: { biz_name: f.biz_name, biz_address: f.biz_address, city: f.city, pull_quote: f.pull_quote }
+ }))
+ });
+ } catch (e: any) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
// Magazine CSV export — every published feature for accountant/CRM/spreadsheet hand-off
app.get('/api/magazine.csv', async (_req, res) => {
try {
@@ -2737,9 +2802,38 @@ app.get('/magazine/:id', async (req, res) => {
) : { rows: [] as any[] };
const esc = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]!));
res.setHeader('Content-Type', 'text/html; charset=utf-8');
+ const baseUrl = process.env.PUBLIC_BASE_URL || 'http://127.0.0.1:9780';
+ const desc = (f.subhead || (f.editorial || '').slice(0, 160) || '').replace(/\s+/g, ' ').trim();
res.send(`<!doctype html><html lang="en"><head><meta charset="utf-8">
<title>${esc(f.headline || f.biz_name)} — The Corridor</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
+<meta name="description" content="${esc(desc).slice(0, 200)}">
+<meta property="og:type" content="article">
+<meta property="og:title" content="${esc(f.headline || f.biz_name)}">
+<meta property="og:description" content="${esc(desc).slice(0, 200)}">
+<meta property="og:url" content="${baseUrl}/magazine/${id}">
+<meta property="og:site_name" content="The Corridor">
+<meta property="og:image" content="${baseUrl}/share/${id}">
+<meta property="og:image:width" content="1080">
+<meta property="og:image:height" content="1920">
+<meta property="article:section" content="${esc(f.category_tag || 'feature')}">
+<meta property="article:published_time" content="${f.published_at ? new Date(f.published_at).toISOString() : new Date(f.generated_at).toISOString()}">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="${esc(f.headline || f.biz_name)}">
+<meta name="twitter:description" content="${esc(desc).slice(0, 200)}">
+<meta name="twitter:image" content="${baseUrl}/share/${id}">
+<script type="application/ld+json">${JSON.stringify({
+ '@context': 'https://schema.org',
+ '@type': 'NewsArticle',
+ headline: f.headline || f.biz_name,
+ description: desc,
+ datePublished: f.published_at ? new Date(f.published_at).toISOString() : new Date(f.generated_at).toISOString(),
+ author: { '@type': 'Organization', name: 'The Corridor desk' },
+ publisher: { '@type': 'Organization', name: 'The Corridor', logo: { '@type': 'ImageObject', url: `${baseUrl}/favicon.svg` } },
+ mainEntityOfPage: `${baseUrl}/magazine/${id}`,
+ articleSection: f.category_tag || 'feature',
+ about: { '@type': 'LocalBusiness', name: f.biz_name, address: f.biz_address || '' }
+})}</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&family=JetBrains+Mono:wght@200;300&display=swap');
:root { --paper:#faf6ee; --ink:#1a1815; --ink-mute:#6e6356; --metal:#8a6d3b; --accent:#6a3a1a; --rule:#d8cdb8; }
@@ -2770,8 +2864,8 @@ 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="/sponsor/${id}" style="color:var(--accent,#6a3a1a);border-color:var(--accent,#6a3a1a)">⭐ Sponsor</a>
+ <a href="/api/magazine/${id}/tap?to=share&dest=${encodeURIComponent(`${baseUrl}/share/${id}`)}" target="_blank">📱 IG share-card</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>
</div>
← 0bc6fd6 iter 113+114: pitch_event_log audit panel on /magazine/:id +
·
back to Ventura Corridor
·
iter 118+119: /magazine.html admin meta strip + /buildings/: e2338c1 →