homepage: auto-promote advertiser stories + premium slots in Industry News

Three-tier sort on the Industry News feed:
1. Premium — articles pinned via adv.featured_story_slots (paid slots).
   Sorted by position ascending. Highlighted with an amber left-border
   gradient + ★ Featured ribbon.
2. Advertiser — articles whose title contains the company name of a
   current active banner advertiser on this property, AND were published
   in the last 48 hours. Highlighted with a blue left-border gradient
   + ◆ Advertiser ribbon. Multiple matches ordered by publish date.
3. General — everything else, newest first.

After 48h, advertiser stories naturally drop back into the general
tier. Premium slots fall off when their ends_at passes.

Detection is title-only with case-insensitive word-boundary match; the
longest matching company name wins so "Sony Pictures" beats "Sony"
when both advertise. Backed by the new adv.active_advertisers_by_
property view.

createAdminClient() now accepts an optional schema arg so we can hit
adv from BB without juggling clients.

API revalidate dropped 300 → 60s so the feed reflects campaign flips
within a minute.
This commit is contained in:
Ryan Salazar
2026-05-27 12:25:20 +00:00
parent d82b8edd21
commit c1ab36297c
4 changed files with 227 additions and 10 deletions

View File

@@ -1,11 +1,11 @@
import { NextResponse } from "next/server";
import { getLatestImportedArticles } from "@/lib/articles/legacy-source";
import { decorateHomepageFeed } from "@/lib/promoted-articles";
export const revalidate = 300;
// Lowered from 300 → 60 so the promoted-story sort reflects current
// advertiser state within a minute of a campaign flip.
export const revalidate = 60;
// Homepage feed now renders every imported post; pagination + infinite-
// scroll happen client-side over the full set. Keep an absolute ceiling
// only as a safety valve, not a UX cap.
const MAX_LIMIT = 10000;
const DEFAULT_LIMIT = 100;
@@ -20,7 +20,7 @@ export async function GET(request: Request) {
const { items, total } = await getLatestImportedArticles(limit, offset);
const payload = items.map((a) => ({
const base = items.map((a) => ({
title: a.title,
excerpt: a.excerpt,
category: a.category,
@@ -33,6 +33,16 @@ export async function GET(request: Request) {
source: "imported" as const,
}));
// Decorate + reorder so premium and current-advertiser stories surface
// at the top of the feed.
const decorated = await decorateHomepageFeed("broadcastbeat", base);
const payload = decorated.map((d) => ({
...d.article,
promoted_kind: d.promoted_kind,
promoted_advertiser: d.promoted_advertiser || null,
promoted_until: d.promoted_until || null,
}));
return NextResponse.json(
{
items: payload,
@@ -43,7 +53,7 @@ export async function GET(request: Request) {
},
{
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=120",
},
},
);