Rene Fichtmueller de179c4c7c fix: remove DEMO labels from real stock data; fix switch Flexoptix suggestions; enrich Hot Topics LLM context
Stock dashboard (index.html):
- Replace all [DEMO]/demo badges on warehouse data with "FS.com" source labels
  (data was always real scraper data, never demo in the DB)
- Update subtitle: "Scraper-Lagermengen: DEMO DATA" → "Wettbewerber-Marktdaten"
- "Recently Restocked" badge: DEMO DATA → SCRAPER DATA

Switch detail (queries.ts):
- Fix getFlexoptixSuggestions: wavelength_nm → wavelength_tx_nm,
  price_verified_usd → street_price_usd (column mismatch with live schema)
- DS5000 and other OSFP switches now show all 62 Flexoptix OSFP transceivers
  with direct shop links in the detail modal

Hot Topics (hot-topics.ts):
- NOG Talks + News Article clusters now fetch summary/mentioned_vendors/
  mentioned_products/mentioned_standards from news_articles table
- description field builds bullet-point list per article with summaries,
  key vendors/standards (vs. 3 bare titles joined with "|" before)
- buildTopicBriefing() rewritten as structured LLM document with sections:
  Market Signals (bullets), Recommended Angle, Market Context (buy signal,
  technologies, impact horizon), Writing Instructions (600-900 words,
  actionable, opinionated, no generic summaries)
2026-05-14 00:33:45 +02:00

890 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Hot Topics Engine — Discovers trending blog topics from multiple sources
*
* Sources:
* 1. OFC/ECOC/CIOE conference papers + announcements
* 2. Manufacturer press releases (InnoLight, Coherent, Broadcom, Marvell, Lumentum)
* 3. Trade press (Lightwave, Fibre Systems, Gazettabyte, LightCounting)
* 4. arXiv papers (cs.NI, eess.SP, physics.optics)
* 5. Internal: price movements, new competitor products, hype cycle shifts
* 6. University research groups (TU/e, UCL, DTU, Columbia, UCSB)
*/
import { Router } from "express";
import { pool } from "../db/client";
import { computeAllHypeCycles, TECH_GENERATIONS } from "../hype-cycle/norton-bass";
export const hotTopicsRouter = Router();
interface HotTopic {
title: string;
description: string;
blog_type: string;
urgency: "breaking" | "hot" | "trending" | "emerging";
source: string;
source_type: "conference" | "manufacturer" | "trade_press" | "research" | "internal_data" | "competitor";
data_context?: Record<string, unknown>;
suggested_angle?: string;
date?: string;
blog_title_created?: boolean;
last_blog_created_at?: string;
rank_score?: number;
llm_context?: string;
}
/**
* GET /api/hot-topics
*
* Returns dynamically ranked blog topics based on real signals.
*/
hotTopicsRouter.get("/", async (req, res) => {
try {
const topics: HotTopic[] = [];
const year = new Date().getFullYear();
const limit = Math.max(1, Math.min(50, parseInt(String(req.query.limit || "20"), 10) || 20));
// ═══ SOURCE 1: Internal Data — Price Movements ═══
const priceDrops = await pool.query(`
SELECT v.name AS vendor, t.form_factor, t.speed_gbps, t.reach_label,
pc.old_price, pc.new_price, pc.delta_pct, pc.currency, pc.detected_at
FROM price_changes pc
JOIN vendors v ON pc.vendor_id = v.id
JOIN transceivers t ON pc.transceiver_id = t.id
WHERE pc.delta_pct < -10 AND pc.detected_at > NOW() - INTERVAL '14 days'
ORDER BY pc.delta_pct ASC LIMIT 5
`).catch(() => ({ rows: [] }));
for (const drop of priceDrops.rows) {
topics.push({
title: `${drop.vendor} drops ${drop.form_factor} ${drop.speed_gbps}G prices by ${Math.abs(Math.round(drop.delta_pct))}%`,
description: `${drop.form_factor} ${drop.speed_gbps}G ${drop.reach_label} went from ${drop.currency} ${drop.old_price} to ${drop.currency} ${drop.new_price}. Market signal or clearance?`,
blog_type: "market_alert",
urgency: Math.abs(drop.delta_pct) > 20 ? "breaking" : "hot",
source: drop.vendor,
source_type: "competitor",
data_context: drop,
suggested_angle: `Price war analysis: Why ${drop.vendor} is cutting ${drop.speed_gbps}G pricing and what it means for procurement`,
date: drop.detected_at ? new Date(drop.detected_at).toISOString() : undefined,
});
}
// ═══ SOURCE 2: Internal Data — New Competitor Products ═══
const newProducts = await pool.query(`
SELECT ca.product_name, ca.form_factor, ca.speed_gbps, ca.source_url,
v.name AS vendor, ca.created_at
FROM competitor_alerts ca
JOIN vendors v ON ca.vendor_id = v.id
WHERE ca.alert_type = 'new_product' AND ca.created_at > NOW() - INTERVAL '14 days'
ORDER BY ca.speed_gbps DESC, ca.created_at DESC LIMIT 5
`).catch(() => ({ rows: [] }));
if (newProducts.rows.length > 0) {
const vendors = [...new Set(newProducts.rows.map(p => p.vendor))];
const speeds = [...new Set(newProducts.rows.map(p => p.speed_gbps + "G"))];
topics.push({
title: `${newProducts.rows.length} new transceiver products from ${vendors.slice(0, 3).join(", ")}`,
description: `New ${speeds.join("/")} products spotted. Competitive landscape shifting.`,
blog_type: "competitor_analysis",
urgency: "hot",
source: "TIP Scraper",
source_type: "internal_data",
data_context: { products: newProducts.rows },
suggested_angle: `Competitor roundup: What ${vendors[0]} and others just launched — and what it means for your next PO`,
date: newProducts.rows[0]?.created_at ? new Date(newProducts.rows[0].created_at).toISOString() : undefined,
});
}
// ═══ SOURCE 3: Internal Data — Hype Cycle Phase Transitions ═══
const hypes = computeAllHypeCycles(year);
const transitions = hypes.filter(h => {
const tech = TECH_GENERATIONS.find(t => t.name === h.technology);
if (!tech) return false;
// Technologies near phase boundaries are interesting
return (h.positionPct > 25 && h.positionPct < 35) || // Entering trough
(h.positionPct > 48 && h.positionPct < 55) || // Leaving trough
(h.positionPct > 78 && h.positionPct < 85); // Entering plateau
});
for (const t of transitions.slice(0, 2)) {
const phaseName = t.phase.replace(/_/g, " ").toLowerCase();
topics.push({
title: `${t.technology} entering ${phaseName}`,
description: `Adoption at ${Math.round(t.adoptionPct)}%. This phase transition means pricing and availability shifts.`,
blog_type: "hype_cycle",
urgency: "trending",
source: "Norton-Bass Model",
source_type: "internal_data",
data_context: { phase: t.phase, adoption: t.adoptionPct, position: t.positionPct },
suggested_angle: `${t.technology} phase shift: What it means for your ${year} infrastructure budget`,
});
}
// ═══ SOURCE 3b: Market Intelligence — Real scraped signals ═══
const marketIntel = await pool.query(`
SELECT title, summary, intel_type, relevance_score, buy_signal_implication,
technologies, source_name, published_at, impact_horizon_months
FROM market_intelligence
WHERE relevance_score > 0.6
ORDER BY relevance_score DESC, published_at DESC NULLS LAST
LIMIT 12
`).catch(() => ({ rows: [] }));
const intelTypeToUrgency: Record<string, HotTopic["urgency"]> = {
supply_chain: "hot", distributor_lead_time: "hot", capex_cycle: "trending",
standard_draft: "emerging", standard_ratified: "trending", trade_show: "hot",
tender: "trending", market_share: "trending", technology_launch: "hot", price_movement: "breaking"
};
const intelTypeToBlogType: Record<string, string> = {
supply_chain: "market_alert", distributor_lead_time: "market_alert", capex_cycle: "market_alert",
standard_draft: "technology_deep_dive", standard_ratified: "technology_deep_dive",
trade_show: "technology_deep_dive", tender: "market_alert", price_movement: "market_alert",
};
const buySignalToAngle: Record<string, string> = {
bullish: "Why now is the right time to buy",
bearish: "Why you should wait before ordering",
opportunity: "Strategic window: Short-term opportunity for procurement",
neutral: "Market context for your next procurement decision",
};
for (const m of marketIntel.rows) {
if (!isUsableMarketIntel(m)) continue;
const techList = Array.isArray(m.technologies) ? (m.technologies as string[]).join(", ") : "";
const angle = m.buy_signal_implication
? buySignalToAngle[m.buy_signal_implication] || m.buy_signal_implication
: "What this means for your network planning";
topics.push({
title: m.title,
description: m.summary || `${m.intel_type?.replace(/_/g, " ")} signal from ${m.source_name || "market data"}.`,
blog_type: intelTypeToBlogType[m.intel_type] || "market_alert",
urgency: intelTypeToUrgency[m.intel_type] || "trending",
source: m.source_name || "Market Intelligence",
source_type: "trade_press",
data_context: {
intel_type: m.intel_type,
relevance: m.relevance_score,
buy_signal: m.buy_signal_implication,
technologies: techList,
impact_months: m.impact_horizon_months,
},
suggested_angle: `${m.title}: ${angle}`,
date: m.published_at ? new Date(m.published_at).toISOString() : undefined,
});
}
// ═══ SOURCE 3c: NOG Conference Talks — scraped from NOG agendas ═══
const nogTalks = await pool.query(`
SELECT title, source, source_url, published_at, relevance_score,
summary, mentioned_vendors, mentioned_products, mentioned_standards
FROM news_articles
WHERE source LIKE 'NOG Talks:%'
AND relevance_score > 0.4
AND published_at > NOW() - INTERVAL '6 months'
ORDER BY relevance_score DESC, published_at DESC NULLS LAST
LIMIT 8
`).catch(() => ({ rows: [] }));
// Cluster NOG talks by NOG name
type NogRow = (typeof nogTalks.rows)[number];
const nogByEvent: Record<string, NogRow[]> = {};
for (const n of nogTalks.rows) {
const event = (n.source as string).replace("NOG Talks: ", "");
if (!nogByEvent[event]) nogByEvent[event] = [];
nogByEvent[event].push(n);
}
for (const [event, talks] of Object.entries(nogByEvent)) {
const topTalk = (talks as NogRow[])[0];
const talkBullets = (talks as NogRow[]).slice(0, 5).map(t => {
const vendors = Array.isArray(t.mentioned_vendors) ? (t.mentioned_vendors as string[]).slice(0, 3).join(", ") : "";
const products = Array.isArray(t.mentioned_products) ? (t.mentioned_products as string[]).slice(0, 3).join(", ") : "";
const extra = [vendors, products].filter(Boolean).join(" / ");
return `${t.title}${extra ? ` (${extra})` : ""}${t.summary ? `${String(t.summary).slice(0, 120)}` : ""}`;
}).join("\n");
topics.push({
title: talks.length === 1
? `[${event}] ${topTalk.title}`
: `${event}: ${talks.length} optics-relevant talks`,
description: talkBullets || (talks as NogRow[]).map(t => t.title).slice(0, 3).join(" | "),
blog_type: "technology_deep_dive",
urgency: "hot",
source: event,
source_type: "conference",
data_context: { talks: (talks as NogRow[]).slice(0, 3) },
suggested_angle: `What ${event} presenters are actually deploying — lessons for your network refresh`,
date: topTalk.published_at ? new Date(topTalk.published_at).toISOString() : undefined,
});
}
// ═══ SOURCE 4: News Articles — Recent Industry News ═══
const recentNews = await pool.query(`
SELECT title, source, source_url, category, published_at,
COALESCE(relevance_score, 5) AS relevance,
summary, mentioned_vendors, mentioned_products, mentioned_standards, tags
FROM news_articles
WHERE source NOT LIKE 'NOG Talks:%'
AND published_at > NOW() - INTERVAL '14 days'
ORDER BY relevance_score DESC NULLS LAST, published_at DESC
LIMIT 12
`).catch(() => ({ rows: [] }));
// Cluster news by theme
type NewsRow = (typeof recentNews.rows)[number];
const newsThemes: Record<string, NewsRow[]> = {};
for (const n of recentNews.rows) {
const theme = detectNewsTheme(n.title);
if (!newsThemes[theme]) newsThemes[theme] = [];
newsThemes[theme].push(n);
}
for (const [theme, articles] of Object.entries(newsThemes)) {
if (articles.length >= 1) {
// Build rich description with article summaries, vendors, standards mentioned
const articleBullets = (articles as NewsRow[]).slice(0, 5).map(a => {
const vendors = Array.isArray(a.mentioned_vendors) ? (a.mentioned_vendors as string[]).slice(0, 3).join(", ") : "";
const stds = Array.isArray(a.mentioned_standards) ? (a.mentioned_standards as string[]).slice(0, 2).join(", ") : "";
const meta = [vendors, stds].filter(Boolean).join(" / ");
return `${a.title}${meta ? ` [${meta}]` : ""}${a.summary ? `${String(a.summary).slice(0, 150)}` : ""}`;
}).join("\n");
topics.push({
title: `${theme}: ${articles.length} recent articles`,
description: articleBullets || articles.map(a => a.title).slice(0, 3).join(" | "),
blog_type: "technology_deep_dive",
urgency: "trending",
source: articles.map(a => a.source).filter(Boolean).slice(0, 2).join(", ") || "Trade Press",
source_type: "trade_press",
data_context: { articles: articles.slice(0, 5) },
suggested_angle: `${theme}: What the latest announcements actually mean for network operators`,
date: articles[0]?.published_at ? new Date(articles[0].published_at).toISOString() : undefined,
});
}
}
// ═══ SOURCE 5: Conference Calendar — Upcoming/Recent Events ═══
const conferences = getConferenceTopics(year);
topics.push(...conferences);
// ═══ SOURCE 6: Emerging Tech — Research-Driven Topics ═══
topics.push(...getResearchTopics(year));
// ═══ SOURCE 7: Evergreen High-Value Topics ═══
topics.push(...getEvergreenTopics(year));
const cleanTopics = dedupeTopics(topics.filter(isUsableTopic));
// Mark already-created topics and rank with daily rotation + source diversity.
const recentDrafts = await pool.query(`
SELECT title, created_at
FROM blog_drafts
WHERE created_at > NOW() - INTERVAL '180 days'
ORDER BY created_at DESC
`).catch(() => ({ rows: [] }));
const createdByTitle = new Map<string, string>();
for (const draft of recentDrafts.rows) {
const key = normalizeTopicTitle(String(draft.title || ""));
if (key && !createdByTitle.has(key)) {
createdByTitle.set(key, draft.created_at ? new Date(draft.created_at).toISOString() : new Date().toISOString());
}
}
const daySeed = getDaySeed();
const rotationSeed = daySeed + getQuerySeed(req.query.shuffle);
for (const topic of cleanTopics) {
const createdAt = createdByTitle.get(normalizeTopicTitle(topic.title));
topic.blog_title_created = Boolean(createdAt);
topic.last_blog_created_at = createdAt;
topic.rank_score = scoreTopic(topic, rotationSeed);
topic.llm_context = buildTopicBriefing(topic);
}
const rankedTopics = selectDiverseTopics(cleanTopics, limit);
// Next daily rotation: tomorrow 00:00 UTC
const tomorrow = new Date();
tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
tomorrow.setUTCHours(0, 0, 0, 0);
res.json({
topics: rankedTopics,
total: cleanTopics.length,
generated_at: new Date().toISOString(),
refreshes_at: tomorrow.toISOString(),
day_seed: daySeed,
rotation_seed: rotationSeed,
sources: ["market_intelligence", "nog_talks", "internal_price_data", "competitor_alerts", "hype_cycle_model", "news_articles", "conference_calendar", "research_papers"],
});
} catch (err) {
console.error("Hot topics error:", err);
res.status(500).json({ error: "Internal server error" });
}
});
function isUsableMarketIntel(row: Record<string, unknown>): boolean {
const text = `${row.title || ""} ${row.summary || ""} ${row.source_name || ""}`.toLowerCase();
const tech = Array.isArray(row.technologies) ? row.technologies.join(" ").toLowerCase() : String(row.technologies || "").toLowerCase();
const rejectPatterns = [
/privacy shield/,
/personal data/,
/data transfer framework/,
/gdpr/,
/cookie/,
/privacy policy/,
/operational amplifier/,
/\boamp\b/,
];
if (rejectPatterns.some((pattern) => pattern.test(text))) return false;
const opticalSignals = [
"transceiver", "optic", "optical", "photonics", "fiber", "fibre", "400g", "800g",
"1.6t", "qsfp", "osfp", "sfp", "coherent", "zr", "cpo", "lpo", "silicon photonics",
"data center", "datacenter", "ai fabric", "ethernet", "dci",
];
return opticalSignals.some((signal) => text.includes(signal) || tech.includes(signal));
}
function isUsableTopic(topic: HotTopic): boolean {
const text = `${topic.title} ${topic.description} ${topic.source}`.toLowerCase();
return ![
/privacy shield/,
/personal data/,
/data transfer framework/,
/gdpr/,
/cookie/,
/privacy policy/,
/operational amplifier/,
/\boamp\b/,
].some((pattern) => pattern.test(text));
}
function dedupeTopics(topics: HotTopic[]): HotTopic[] {
const byKey = new Map<string, HotTopic>();
for (const topic of topics) {
const key = normalizeTopicTitle(topic.title);
const previous = byKey.get(key);
if (!previous || scorePreRankTopic(topic) > scorePreRankTopic(previous)) {
byKey.set(key, topic);
}
}
return Array.from(byKey.values());
}
function scorePreRankTopic(topic: HotTopic): number {
const urgencyScore: Record<HotTopic["urgency"], number> = {
breaking: 4,
hot: 3,
trending: 2,
emerging: 1,
};
const freshness = topic.date ? Math.max(0, 30 - ((Date.now() - new Date(topic.date).getTime()) / 86400000)) : 0;
const dataScore = topic.data_context ? 1 : 0;
return (urgencyScore[topic.urgency] || 0) * 100 + freshness + dataScore;
}
function normalizeTopicTitle(title: string): string {
return title
.toLowerCase()
.replace(/\b20\d{2}\b/g, "{year}")
.replace(/[^a-z0-9]+/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function seededTopicJitter(title: string, seed: number): number {
let s = seed;
const normalized = normalizeTopicTitle(title);
for (let i = 0; i < normalized.length; i++) {
s = (s * 1664525 + normalized.charCodeAt(i) + 1013904223) & 0x7fffffff;
}
return s % 140;
}
function getQuerySeed(value: unknown): number {
if (!value) return 0;
const raw = Array.isArray(value) ? String(value[0] || "") : String(value);
if (!raw) return 0;
let hash = 0;
for (let i = 0; i < raw.length; i++) {
hash = ((hash << 5) - hash + raw.charCodeAt(i)) | 0;
}
return Math.abs(hash % 100000);
}
function compactDataContext(data: Record<string, unknown> | undefined): string {
if (!data) return "";
const replacer = (_key: string, value: unknown) => {
if (Array.isArray(value)) return value.slice(0, 5);
if (typeof value === "string" && value.length > 260) return `${value.slice(0, 257)}...`;
return value;
};
return JSON.stringify(data, replacer, 2).slice(0, 1800);
}
function buildTopicBriefing(topic: HotTopic): string {
const lines = [
`=== BLOG BRIEFING: ${topic.title} ===`,
``,
`Urgency: ${topic.urgency.toUpperCase()}`,
`Source category: ${topic.source_type} | Source: ${topic.source}`,
];
if (topic.date) lines.push(`Signal date: ${new Date(topic.date).toLocaleDateString("de-DE", { day: "2-digit", month: "long", year: "numeric" })}`);
// Core signal content — article bullets or summary
if (topic.description) {
lines.push(``, `--- Market Signals ---`);
// Already formatted as bullets for news/nog, or plain summary for market intel
lines.push(topic.description.includes("•") ? topic.description : `Signal summary: ${topic.description}`);
}
// Recommended editorial angle
if (topic.suggested_angle) {
lines.push(``, `--- Recommended Blog Angle ---`);
lines.push(topic.suggested_angle);
}
// Structured data from data_context (vendors, tech, buy signal, etc.)
const ctx = topic.data_context;
if (ctx) {
const extraLines: string[] = [];
if (ctx.buy_signal && typeof ctx.buy_signal === "string") {
const signalMap: Record<string, string> = { bullish: "BUY signal — demand growing, order soon", bearish: "WAIT signal — pricing softening or supply improving", opportunity: "SHORT-TERM OPPORTUNITY — act now", neutral: "Monitor — no immediate action needed" };
extraLines.push(`Buy signal: ${signalMap[ctx.buy_signal] ?? ctx.buy_signal}`);
}
if (ctx.technologies && String(ctx.technologies).length > 2) extraLines.push(`Key technologies: ${ctx.technologies}`);
if (ctx.impact_months) extraLines.push(`Expected market impact: within ${ctx.impact_months} months`);
if (extraLines.length > 0) {
lines.push(``, `--- Market Context ---`);
lines.push(...extraLines);
}
}
if (topic.blog_title_created && topic.last_blog_created_at) {
lines.push(``, `⚠ Editorial note: A similar blog already exists (created ${new Date(topic.last_blog_created_at).toLocaleDateString("de-DE")}). Choose a materially different angle — different structure, timeframe, or use-case focus.`);
}
lines.push(``, `--- Writing Instructions ---`);
lines.push(`Write a practical optical networking article (600900 words) that a network engineer or procurement manager at an ISP, cloud provider, or enterprise can immediately use. Include:`);
lines.push(`1. What is actually happening in the market (fact-based, no generic intro)`);
lines.push(`2. Specific technical implications (form factors, speeds, reach, protocol implications)`);
lines.push(`3. Procurement/planning consequences — what to order, what to delay, what to watch`);
lines.push(`4. One concrete recommendation or action item`);
lines.push(`Do NOT write generic summaries or restate the title. Be opinionated and specific.`);
return lines.join("\n");
}
function scoreTopic(topic: HotTopic, seed: number): number {
const urgencyScore: Record<HotTopic["urgency"], number> = {
breaking: 900,
hot: 760,
trending: 620,
emerging: 500,
};
const sourceScore: Record<HotTopic["source_type"], number> = {
internal_data: 90,
competitor: 85,
trade_press: 75,
conference: 70,
manufacturer: 65,
research: 60,
};
let freshness = 0;
if (topic.date) {
const ageDays = Math.max(0, (Date.now() - new Date(topic.date).getTime()) / 86400000);
freshness = Math.max(0, 90 - ageDays * 4);
}
const createdPenalty = topic.blog_title_created ? -950 : 0;
return (
(urgencyScore[topic.urgency] ?? 400) +
(sourceScore[topic.source_type] ?? 40) +
freshness +
seededTopicJitter(topic.title, seed) +
createdPenalty
);
}
function selectDiverseTopics(topics: HotTopic[], limit: number): HotTopic[] {
const sorted = [...topics].sort((a, b) => (b.rank_score ?? 0) - (a.rank_score ?? 0));
const selected: HotTopic[] = [];
const sourceTypeCount = new Map<string, number>();
const sourceCount = new Map<string, number>();
for (const topic of sorted) {
if (selected.length >= limit) break;
const sourceType = topic.source_type;
const source = topic.source || "unknown";
const typeCount = sourceTypeCount.get(sourceType) ?? 0;
const srcCount = sourceCount.get(source) ?? 0;
if (typeCount >= 5) continue;
if (srcCount >= 3) continue;
selected.push(topic);
sourceTypeCount.set(sourceType, typeCount + 1);
sourceCount.set(source, srcCount + 1);
}
if (selected.length < limit) {
for (const topic of sorted) {
if (selected.length >= limit) break;
if (!selected.includes(topic)) selected.push(topic);
}
}
return selected;
}
function detectNewsTheme(title: string): string {
const tl = title.toLowerCase();
if (tl.includes("800g") || tl.includes("osfp")) return "800G Deployment Wave";
if (tl.includes("1.6t") || tl.includes("1.6 t")) return "1.6T Race";
if (tl.includes("silicon photonics") || tl.includes("sipho")) return "Silicon Photonics";
if (tl.includes("cpo") || tl.includes("co-packaged")) return "Co-Packaged Optics";
if (tl.includes("lpo") || tl.includes("linear")) return "Linear-Drive Pluggable";
if (tl.includes("400zr") || tl.includes("coherent")) return "Coherent Pluggable";
if (tl.includes("ai") || tl.includes("gpu") || tl.includes("ml")) return "AI/ML Fabric Optics";
if (tl.includes("innolight") || tl.includes("coherent corp") || tl.includes("broadcom")) return "Manufacturer Moves";
if (tl.includes("supply") || tl.includes("shortage") || tl.includes("lead time")) return "Supply Chain Alert";
return "Industry Update";
}
function getConferenceTopics(year: number): HotTopic[] {
const now = new Date();
const month = now.getMonth() + 1;
const topics: HotTopic[] = [];
// OFC = March, ECOC = September, CIOE = September, Photonics West = January
if (month >= 1 && month <= 2) {
topics.push({
title: `Photonics West ${year}: What the Laser and Photonics Research Means for Transceivers`,
description: "SPIE Photonics West is where the component-level research lands first. What was shown and how long until it ships?",
blog_type: "technology_deep_dive",
urgency: month === 1 ? "hot" : "trending",
source: "SPIE Photonics West",
source_type: "conference",
suggested_angle: "Photonics West signal reading: The research demos that become products in 2-3 years",
});
}
if (month >= 2 && month <= 4) {
topics.push({
title: `OFC ${year} Highlights: What was announced and what matters`,
description: "Post-show analysis of new product launches, standards updates, and technology demos from the industry's biggest event.",
blog_type: "technology_deep_dive",
urgency: month === 3 ? "breaking" : "hot",
source: "OFC Conference",
source_type: "conference",
suggested_angle: "OFC show floor reality: 5 announcements that actually change your procurement strategy",
});
}
if (month >= 5 && month <= 7) {
topics.push({
title: `NFOEC / OFC ${year} Follow-Up: Which Products Actually Shipped?`,
description: "Six months after OFC demos — checking which announcements turned into shipping hardware and which were still vapor.",
blog_type: "market_alert",
urgency: "trending",
source: "OFC Conference Follow-Up",
source_type: "conference",
suggested_angle: "Post-OFC reality check: Announcement vs reality 6 months later",
});
}
if (month >= 8 && month <= 10) {
topics.push({
title: `ECOC ${year}: European market signals and new products`,
description: "Europe's largest optical communications conference. Key for understanding EU market direction.",
blog_type: "technology_deep_dive",
urgency: month === 9 ? "breaking" : "trending",
source: "ECOC Conference",
source_type: "conference",
suggested_angle: "ECOC takeaways: What European carriers are actually deploying (not just demoing)",
});
}
if (month >= 9 && month <= 11) {
topics.push({
title: `CIOE ${year} Shenzhen: Chinese Manufacturers Show Their Cards`,
description: "CIOE is where InnoLight, HG Genuine, and Chinese OEMs announce next-gen products. First look at the 2025 roadmap.",
blog_type: "technology_deep_dive",
urgency: "trending",
source: "CIOE Shenzhen",
source_type: "conference",
suggested_angle: "CIOE market signal: What Chinese manufacturers shipping in 12 months means for Western pricing",
});
}
if (month >= 11 || month <= 1) {
topics.push({
title: `${year} Year-End Optics Market Review: Who Won, Who Lost`,
description: "Annual review: which speeds gained share, which vendors shipped on roadmap, and what the price curves looked like.",
blog_type: "market_alert",
urgency: "hot",
source: "TIP Market Data",
source_type: "internal_data",
suggested_angle: `Year-end optics market: The numbers that matter for your ${year + 1} budget`,
});
}
// Always relevant — rotate between 3 variants using day seed
const standardsTopics: HotTopic[] = [
{
title: `${year} Standards Update: IEEE 802.3 / OIF / MSA tracker`,
description: "What's ratified, what's in draft, and what it means for your next-gen optics roadmap.",
blog_type: "technology_deep_dive",
urgency: "trending",
source: "IEEE / OIF / MSA",
source_type: "research",
suggested_angle: "Standards reality check: Which specs are production-ready vs. still in committee",
},
{
title: `SFF-8024 Transceiver Management Update ${year}: What's New in the Identifier Registry`,
description: "The SFF Committee's transceiver identifier registry updated again. New form factors, management interfaces, and what switches need firmware updates to support them.",
blog_type: "technology_deep_dive",
urgency: "trending",
source: "SFF Committee",
source_type: "research",
suggested_angle: "SFF registry changes: The low-level updates that cause high-level compatibility headaches",
},
{
title: `OIF CEI-112G Standard: What It Means for the Next Generation of DAC and AOC`,
description: "CEI-112G is the electrical interface spec that enables 112G PAM4 — the building block of 400G and 800G. What it enables and when it matters.",
blog_type: "technology_deep_dive",
urgency: "emerging",
source: "OIF CEI",
source_type: "research",
suggested_angle: "CEI-112G explained: Why the electrical interface standard matters more than the optics spec",
},
];
const seed = getDaySeed() + 3;
topics.push(seededShuffle(standardsTopics, seed)[0]);
return topics;
}
/**
* Seeded shuffle — same seed = same order. Uses day-of-year as seed so
* topics rotate daily but are stable within the same day.
*/
function seededShuffle<T>(arr: T[], seed: number): T[] {
const out = [...arr];
let s = seed;
for (let i = out.length - 1; i > 0; i--) {
s = (s * 1664525 + 1013904223) & 0x7fffffff;
const j = s % (i + 1);
[out[i], out[j]] = [out[j], out[i]];
}
return out;
}
function getDaySeed(): number {
const now = new Date();
const start = new Date(now.getFullYear(), 0, 0);
const dayOfYear = Math.floor((now.getTime() - start.getTime()) / 86400000);
return now.getFullYear() * 1000 + dayOfYear;
}
function getResearchTopics(year: number): HotTopic[] {
const ALL_RESEARCH: HotTopic[] = [
{
title: "Silicon Photonics: From Lab to Production — Where Are We Really?",
description: "Intel, Broadcom, Marvell, and startups (Lightmatter, Ayar Labs) are all pushing SiPho. But production yields and packaging costs tell a different story.",
blog_type: "technology_deep_dive",
urgency: "trending",
source: "Industry Research",
source_type: "research",
suggested_angle: "Silicon Photonics reality: What works in production vs what's still a conference demo",
},
{
title: "LPO vs DSP: The Power Efficiency Battle That Reshapes Data Centers",
description: "Linear-drive pluggable optics promise 50% power savings by eliminating the DSP. But interop and reach limitations are real.",
blog_type: "comparison",
urgency: "emerging",
source: "OIF / University Research",
source_type: "research",
suggested_angle: "LPO honest assessment: When the power savings justify the interop risk",
},
{
title: `CPO Roadmap ${year}: Co-Packaged Optics Timeline Reality Check`,
description: "Broadcom showed CPO demos at OFC. But what's the real timeline for rack-level deployment?",
blog_type: "hype_cycle",
urgency: "emerging",
source: "Broadcom / Intel / TSMC",
source_type: "manufacturer",
suggested_angle: "CPO: Why it's 3 years away (and has been for the last 5 years)",
},
{
title: "1.6T Optics: Who Ships First and What the Specs Actually Mean",
description: "Vendors are announcing 1.6T but the standards aren't final. Here's what's real, what's vaporware, and what lead times to expect.",
blog_type: "technology_deep_dive",
urgency: "emerging",
source: "OIF / IEEE",
source_type: "research",
suggested_angle: "1.6T reality check: Sorting PR from actual shipping hardware",
},
{
title: "OSFP vs QSFP-DD: The 400G/800G Form Factor Fight Isn't Over",
description: "Both coexist in the market. Hyperscalers went OSFP for 800G; enterprise is on QSFP-DD. What does this mean for your optics inventory?",
blog_type: "comparison",
urgency: "trending",
source: "QSFP-DD MSA / OSFP MSA",
source_type: "research",
suggested_angle: "Form factor fragmentation: How to not end up with the wrong 800G optics",
},
{
title: "Coherent ZR vs ZR+: The Spec Sheet Lies You Need to Know",
description: "OpenZR+ vs 400ZR implementations from different vendors are NOT interoperable by default. The details matter.",
blog_type: "comparison",
urgency: "trending",
source: "OIF Implementation Agreement",
source_type: "research",
suggested_angle: "Coherent interop truth: Why your ZR link won't work with mixed vendors out of the box",
},
{
title: "BiDi Optics: The Most Misunderstood Product in the Catalog",
description: "Single-strand operation sounds simple. The wavelength pairing rules, fiber polarity requirements, and what goes wrong when teams mix Tx/Rx wavelengths.",
blog_type: "tutorial",
urgency: "trending",
source: "Field Cases",
source_type: "internal_data",
suggested_angle: "BiDi done right: Why half your BiDi deployments have the wrong wavelength on the wrong end",
},
{
title: "Transceiver DDM/DOM: How to Actually Use the Diagnostic Data",
description: "Every modern optic reports temperature, voltage, Tx power, Rx power, and bias current. Most teams never check it. Here's what the numbers tell you.",
blog_type: "tutorial",
urgency: "trending",
source: "SFF Committee / Field Data",
source_type: "research",
suggested_angle: "DDM as a maintenance tool: Catching failing optics before they take down your link",
},
{
title: `800G Deployment Wave: What Hyperscalers Are Actually Installing in ${year}`,
description: "Meta, Google, and Microsoft are ordering 800G at scale. What form factors, vendors, and reach variants are winning?",
blog_type: "market_alert",
urgency: "hot",
source: "LightCounting / Hyperscaler CapEx Reports",
source_type: "research",
suggested_angle: "800G market signal: What hyperscaler spending tells enterprise about their 2027 refresh cycle",
},
];
// Pick 3 different research topics per day
const shuffled = seededShuffle(ALL_RESEARCH, getDaySeed() + 1);
return shuffled.slice(0, 3);
}
function getEvergreenTopics(year: number): HotTopic[] {
const ALL_EVERGREEN: HotTopic[] = [
{
title: `The ${year} Transceiver Buying Guide: OEM vs Compatible`,
description: "Updated pricing data, vendor quality tiers, and the procurement playbook for every speed class.",
blog_type: "buying_guide",
urgency: "trending",
source: "TIP Price Data",
source_type: "internal_data",
suggested_angle: "Stop overpaying: The definitive OEM vs compatible decision framework with real numbers",
},
{
title: `400G Migration Playbook: From 100G to 400G in 12 Months`,
description: "Step-by-step migration guide including cabling, switch selection, optics procurement, and what goes wrong.",
blog_type: "migration_guide",
urgency: "trending",
source: "Field Experience",
source_type: "internal_data",
suggested_angle: "Migration horror stories and how to avoid them: The 100G→400G field guide",
},
{
title: "MPO Connector Survival Guide: Polarity, Cleaning, and Why Your Links Keep Dying",
description: "The #1 cause of 40G/100G/400G deployment failures. Everything you need to know about MPO.",
blog_type: "tutorial",
urgency: "trending",
source: "Support Cases",
source_type: "internal_data",
suggested_angle: "Your $350 optic isn't broken — your connector is dirty. The MPO reality guide.",
},
{
title: "Transceiver Compatibility: Why Your New Optic Doesn't Work and How to Fix It",
description: "DOM lockout, software coding, MSA compliance, and the vendor unlock codes that nobody documents.",
blog_type: "tutorial",
urgency: "trending",
source: "Support Cases",
source_type: "internal_data",
suggested_angle: "Compatibility reality: The 4 reasons optics get rejected and exactly how to resolve each one",
},
{
title: `${year} Price Tracker: Which Transceiver Speeds Are Getting Cheaper Fastest?`,
description: "25G SFP28 hit floor pricing. 100G QSFP28 is still dropping. 400G is at inflection. Where to buy now, where to wait.",
blog_type: "market_alert",
urgency: "hot",
source: "TIP Price Observations",
source_type: "internal_data",
suggested_angle: "Buy signal analysis: The optic speeds where waiting 6 months saves 30%",
},
{
title: "Power Budget 101: Why Your 10km LR Link Only Reaches 7km",
description: "TX power, receiver sensitivity, fiber loss, connector loss, and the splice margin most engineers forget to account for.",
blog_type: "tutorial",
urgency: "trending",
source: "Field Experience",
source_type: "internal_data",
suggested_angle: "Loss budget done right: The calculation every engineer should do before ordering LR optics",
},
{
title: "Cisco vs Arista vs Juniper Optics Lock-In: The True Cost Analysis",
description: "OEM optics from major vendors carry a 300-800% premium. Here's the legal situation, the unlock process, and when it's actually worth paying OEM prices.",
blog_type: "buying_guide",
urgency: "trending",
source: "Vendor Pricing / Legal Docs",
source_type: "internal_data",
suggested_angle: "OEM lock-in economics: The numbers behind why compatible optics work and when they don't",
},
{
title: `40G End-of-Life: What to Do With Your QSFP+ Inventory in ${year}`,
description: "40G is in decline. Resale values, upgrade paths to 100G, and when to write off the inventory.",
blog_type: "market_alert",
urgency: "trending",
source: "TIP Price Data",
source_type: "internal_data",
suggested_angle: "40G sunset planning: The migration math that justifies skipping 40G refresh entirely",
},
{
title: "DAC vs AOC vs Optic+Cable: The Short-Reach Decision Tree",
description: "For ≤5m: DAC. For 5-100m: AOC. For >100m: pluggable. But the breakeven points shift with port density and heat budgets.",
blog_type: "comparison",
urgency: "trending",
source: "Field Data",
source_type: "internal_data",
suggested_angle: "Short-reach optics: The decision framework that saves budget without sacrificing reliability",
},
{
title: "Gray Optics: Market Pricing vs New — When the Risk Is Worth It",
description: "eBay, Alibaba, and gray market brokers sell transceiver inventory at 40-70% below retail. What to check, what can go wrong, and what the warranty situation actually is.",
blog_type: "buying_guide",
urgency: "trending",
source: "Market Research",
source_type: "internal_data",
suggested_angle: "Gray market guide: The due diligence checklist that separates a deal from a disaster",
},
{
title: `Wavelength Division Multiplexing in ${year}: CWDM4 vs LWDM vs DWDM Decision Guide`,
description: "WDM optics for data center interconnect. The wavelength plans, the reach limits, and when DWDM pays off vs CWDM.",
blog_type: "technology_deep_dive",
urgency: "trending",
source: "IEEE / OIF",
source_type: "research",
suggested_angle: "WDM selection guide: Matching wavelength technology to your actual DCI requirements",
},
{
title: "Temperature-Hardened Optics: When ETSI Class 4.1 Actually Matters",
description: "Industrial, outdoor, and telecom deployments require optics rated for -40°C to +85°C. What the specs mean and which products are actually qualified.",
blog_type: "buying_guide",
urgency: "emerging",
source: "ETSI / Telecom Field Cases",
source_type: "internal_data",
suggested_angle: "Industrial optics selection: When your data center optic will kill your outdoor deployment",
},
];
// Pick 4 different evergreen topics per day (rotates daily via seeded shuffle)
const shuffled = seededShuffle(ALL_EVERGREEN, getDaySeed() + 2);
return shuffled.slice(0, 4);
}