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:
@@ -8,9 +8,9 @@ const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const SCHEMA = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb";
|
||||
|
||||
function client() {
|
||||
function client(schema = SCHEMA) {
|
||||
return createClient(SUPABASE_URL, SUPABASE_ANON, {
|
||||
db: { schema: SCHEMA as "public" },
|
||||
db: { schema: schema as "public" },
|
||||
auth: { persistSession: false },
|
||||
});
|
||||
}
|
||||
@@ -50,11 +50,51 @@ export async function GET(req: Request) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const q = (searchParams.get("q") || "").trim();
|
||||
if (q.length < 2) {
|
||||
return NextResponse.json({ items: [] });
|
||||
return NextResponse.json({ items: [], companies: [] });
|
||||
}
|
||||
const limit = Math.min(parseInt(searchParams.get("limit") || "8", 10) || 8, 15);
|
||||
const like = `%${q.replace(/[%_]/g, " ")}%`;
|
||||
|
||||
// Parallel: matching directory companies (sponsors first).
|
||||
const companiesPromise = (async () => {
|
||||
try {
|
||||
const sb = client();
|
||||
const { data } = await sb
|
||||
.from("tracked_companies")
|
||||
.select("slug, company_name, logo_url, exhibits_nab, exhibits_ibc")
|
||||
.eq("directory_visible", true)
|
||||
.ilike("company_name", like)
|
||||
.order("featured", { ascending: false })
|
||||
.order("mention_count", { ascending: false })
|
||||
.limit(6);
|
||||
if (!data) return [];
|
||||
|
||||
let sponsors = new Set<string>();
|
||||
try {
|
||||
const adv = client("adv");
|
||||
const { data: ads } = await adv
|
||||
.from("active_advertisers_by_property")
|
||||
.select("company_name_lower")
|
||||
.eq("property", "broadcastbeat");
|
||||
sponsors = new Set((ads || []).map((r: any) => String(r.company_name_lower)));
|
||||
} catch { /* tolerate */ }
|
||||
|
||||
const rows = (data as any[]).map((r) => ({
|
||||
slug: r.slug as string,
|
||||
name: r.company_name as string,
|
||||
logoUrl: (r.logo_url as string) || null,
|
||||
exhibitsNab: !!r.exhibits_nab,
|
||||
exhibitsIbc: !!r.exhibits_ibc,
|
||||
isSponsor: sponsors.has(String(r.company_name || "").toLowerCase()),
|
||||
href: `/manufacturers/${r.slug}`,
|
||||
}));
|
||||
rows.sort((a, b) => (b.isSponsor ? 1 : 0) - (a.isSponsor ? 1 : 0));
|
||||
return rows;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
|
||||
const sb = client();
|
||||
// Match against title, excerpt, content, AND author name (was just
|
||||
// title+author). Content match is what surfaces hits when the phrase
|
||||
@@ -126,8 +166,10 @@ export async function GET(req: Request) {
|
||||
};
|
||||
});
|
||||
|
||||
const companies = await companiesPromise;
|
||||
|
||||
return NextResponse.json(
|
||||
{ items, query: q },
|
||||
{ items, companies, query: q },
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "public, s-maxage=30, stale-while-revalidate=60",
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { Article } from "@/lib/articles/types";
|
||||
interface NewsArticleDetailClientProps {
|
||||
article: Article;
|
||||
relatedArticles: Article[];
|
||||
companiesInStory?: React.ReactNode;
|
||||
}
|
||||
|
||||
function getSessionId(): string {
|
||||
@@ -29,6 +30,7 @@ function getSessionId(): string {
|
||||
export default function NewsArticleDetailClient({
|
||||
article,
|
||||
relatedArticles,
|
||||
companiesInStory,
|
||||
}: NewsArticleDetailClientProps) {
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
const [savingState, setSavingState] = useState<"idle" | "saving" | "removing">("idle");
|
||||
@@ -519,6 +521,9 @@ export default function NewsArticleDetailClient({
|
||||
{/* Blackmagic 300x600 fixed at top + every 300x250 banner stacked */}
|
||||
<SidebarAdStack />
|
||||
|
||||
{/* Companies referenced in the article body */}
|
||||
{companiesInStory}
|
||||
|
||||
{/* Related Articles Sidebar */}
|
||||
<div className="bg-[#111] border border-[#222] p-5">
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
getLegacyRecentSlugs,
|
||||
getLegacyRelatedArticles,
|
||||
} from "@/lib/articles/legacy-source";
|
||||
import { linkifyArticleHtml } from "@/lib/company-mentions";
|
||||
import { linkifyAndExtractMentions } from "@/lib/company-mentions";
|
||||
import CompanyMentionsHover from "@/components/CompanyMentionsHover";
|
||||
import CompaniesInThisStory from "@/components/CompaniesInThisStory";
|
||||
import NewsArticleDetailClient from "./NewsArticleDetailClient";
|
||||
|
||||
// ISR: serve cached pages but revalidate hourly. New imported posts surface
|
||||
@@ -91,9 +92,9 @@ export default async function NewsArticlePage({ params }: PageProps) {
|
||||
redirect("/news");
|
||||
}
|
||||
|
||||
// Auto-link company-name mentions in the article body to /manufacturers/<slug>
|
||||
// with data-company-slug so the hover card can pop a preview.
|
||||
const linkedContent = await linkifyArticleHtml(article.content || "");
|
||||
// Auto-link company-name mentions in the article body and capture the set
|
||||
// of matched slugs for the "Companies in this story" sidebar.
|
||||
const { html: linkedContent, mentionedSlugs } = await linkifyAndExtractMentions(article.content || "");
|
||||
const articleWithLinks: Article = { ...article, content: linkedContent };
|
||||
|
||||
const articleSchema = {
|
||||
@@ -119,7 +120,15 @@ export default async function NewsArticlePage({ params }: PageProps) {
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
|
||||
/>
|
||||
<NewsArticleDetailClient article={articleWithLinks} relatedArticles={related} />
|
||||
<NewsArticleDetailClient
|
||||
article={articleWithLinks}
|
||||
relatedArticles={related}
|
||||
companiesInStory={
|
||||
mentionedSlugs.length > 0 ? (
|
||||
<CompaniesInThisStory slugs={mentionedSlugs} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<CompanyMentionsHover />
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user