cross-link: companies-in-this-story sidebar + unified company search
Feature 1 — sidebar
Adds a "Companies in this story" panel on every news article that
surfaces every directory company my linker matched in the body. Each
tile shows logo, name, Sponsor / NAB 26 / IBC badges, and a one-sentence
bio. Tiles link to /manufacturers/<slug> and carry data-company-slug so
the global hover card pops on hover too.
Extends company-mentions.ts with linkifyAndExtractMentions() that
returns both the linked HTML and the ordered list of matched slugs in
one pass, so /news/[slug] doesn't re-scan the body for the sidebar.
Feature 2 — unified typeahead
/api/search/suggest now returns { companies, items } — companies are
matched against bb.tracked_companies (directory_visible=true), capped
at 6, ordered by featured → mention_count, then bumped if they're an
active advertiser on broadcastbeat (adv schema lookup).
Header search dropdown renders a "Companies" section at the top with
logo, name, Sponsor badge (active advertiser) or NAB 26 badge, above
the existing "Stories" results. Each company link carries
data-company-slug so the hover card works in the dropdown too.
This commit is contained in:
@@ -156,3 +156,53 @@ export async function linkifyArticleHtml(html: string): Promise<string> {
|
||||
const companies = await loadDirectoryCompanies();
|
||||
return linkifyCompanyMentions(html, companies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as linkifyArticleHtml but also returns the set of company slugs that
|
||||
* were matched (in the order they first appeared). Callers use this to
|
||||
* render a "Companies in this story" sidebar without re-scanning the HTML.
|
||||
*/
|
||||
export async function linkifyAndExtractMentions(
|
||||
html: string,
|
||||
): Promise<{ html: string; mentionedSlugs: string[] }> {
|
||||
const companies = await loadDirectoryCompanies();
|
||||
if (!html || companies.length === 0) return { html: html || "", mentionedSlugs: [] };
|
||||
|
||||
// Re-run the same matching against the original (unmodified) HTML so we
|
||||
// can capture the slug list. We need to dupe the logic a bit because the
|
||||
// existing linkifyCompanyMentions function mutates as it goes.
|
||||
const cheerio = require("cheerio");
|
||||
const $ = cheerio.load(html, { decodeEntities: false }, false);
|
||||
const matched: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const SKIP_TAGS = new Set(["a", "script", "style", "code", "pre", "h1", "h2", "h3", "h4", "h5", "h6"]);
|
||||
|
||||
function scan(node: any): void {
|
||||
if (!node) return;
|
||||
if (node.type === "tag") {
|
||||
if (SKIP_TAGS.has(node.name)) return;
|
||||
if (node.attribs && node.attribs["data-no-autolink"] === "true") return;
|
||||
const kids = node.children ? [...node.children] : [];
|
||||
for (const k of kids) scan(k);
|
||||
return;
|
||||
}
|
||||
if (node.type !== "text") return;
|
||||
const text: string = node.data || "";
|
||||
if (!text.trim()) return;
|
||||
for (const c of companies) {
|
||||
if (seen.has(c.slug)) continue;
|
||||
const re = new RegExp(`(^|[^A-Za-z0-9])(${c.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})(?![A-Za-z0-9])`, "i");
|
||||
if (re.test(text)) {
|
||||
matched.push(c.slug);
|
||||
seen.add(c.slug);
|
||||
if (matched.length >= MAX_LINKS_PER_ARTICLE) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$.root().children().each((_: number, el: any) => scan(el));
|
||||
|
||||
const linkedHtml = linkifyCompanyMentions(html, companies);
|
||||
return { html: linkedHtml, mentionedSlugs: matched };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user