KOL count: 29 → 25 across marketing/SEO copy

Backend KOL_FEEDS trimmed from 29 to 25 (dead feeds removed).
Sync all hardcoded count mentions:
- layout.tsx JSON-LD, page.tsx (metric + comparison + copy)
- kol/page.tsx, KolPageClient.tsx ("and 26 more" → "and 22 more")
- glossary/page.tsx, opengraph-image.tsx
- public/llms.txt, llms-full.txt
- drop removed KOLs (Dragonfly Capital, Nic Carter) from named lists

Bundles other in-flight frontend work already in the working tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
k
2026-06-09 22:55:27 +08:00
parent 9e0f6554cb
commit 4c3c8c6f87
57 changed files with 3464 additions and 1855 deletions
+118
View File
@@ -0,0 +1,118 @@
import type { TrumpPost } from '@/types'
import { getPosts, getPostsPage, type PostListResponse } from './api'
const LIVE_ARCHIVE_SOURCES = new Set([
'truth',
'btc_bottom_reversal',
'funding_reversal',
'kol_divergence',
])
function isAiScored(post: Pick<TrumpPost, 'ai_confidence' | 'ai_reasoning'>): boolean {
return (post.ai_confidence ?? 0) > 0 || !!post.ai_reasoning
}
function buildSourceCounts(items: TrumpPost[]): PostListResponse['source_counts'] {
const counts = new Map<string, { count: number; latest: string | null }>()
for (const post of items) {
const hit = counts.get(post.source)
if (!hit) {
counts.set(post.source, { count: 1, latest: post.published_at })
continue
}
hit.count += 1
if (!hit.latest || post.published_at > hit.latest) hit.latest = post.published_at
}
return Array.from(counts.entries())
.map(([source, meta]) => ({ source, count: meta.count, latest: meta.latest }))
.sort((a, b) => b.count - a.count || a.source.localeCompare(b.source))
}
export function buildPostListFallbackResponse(
items: TrumpPost[],
source: string,
limit: number,
): PostListResponse {
return {
items,
total: items.length,
page: 1,
limit,
counts: {
all: items.length,
actionable: items.filter(p => p.signal === 'buy' || p.signal === 'short').length,
buy: items.filter(p => p.signal === 'buy').length,
short: items.filter(p => p.signal === 'short').length,
off_topic: items.filter(p => !isAiScored(p)).length,
},
source_counts: [{ source, count: items.length, latest: items[0]?.published_at ?? null }],
}
}
export function buildArchiveFallbackResponse(
allPosts: TrumpPost[],
page: number,
limit: number,
source = 'all',
): PostListResponse {
const archivePosts = allPosts.filter(post => !LIVE_ARCHIVE_SOURCES.has(post.source))
const sourceCounts = buildSourceCounts(archivePosts)
const filtered = source === 'all'
? archivePosts
: archivePosts.filter(post => post.source === source)
const offset = (page - 1) * limit
const items = filtered.slice(offset, offset + limit)
return {
items,
total: filtered.length,
page,
limit,
counts: {
all: filtered.length,
actionable: filtered.filter(p => p.signal === 'buy' || p.signal === 'short').length,
buy: filtered.filter(p => p.signal === 'buy').length,
short: filtered.filter(p => p.signal === 'short').length,
off_topic: filtered.filter(p => !isAiScored(p)).length,
},
source_counts: sourceCounts,
}
}
export async function getInitialPostPage(
limit: number,
page: number,
options: {
source?: string
filters?: {
sourceIn?: string[]
sourceNotIn?: string[]
archiveOnly?: boolean
sentiment?: 'bullish' | 'bearish' | 'neutral'
signal?: 'buy' | 'short' | 'actionable'
aiScoredOnly?: boolean
}
legacyFallbackSource?: string
legacyFallback?: () => Promise<PostListResponse | null>
},
): Promise<PostListResponse | null> {
return getPostsPage(limit, page, options.source, options.filters).catch(async (e) => {
// Only fall back to legacy /posts on 404 (new endpoint not yet deployed).
// Non-404 errors (500, network) should NOT silently mask as empty data —
// a server-side fallback on 500 produces initial HTML that the client
// cannot reproduce on re-fetch (it shows an error instead), causing a
// hydration mismatch. Return null so the page renders empty and the
// client fetches cleanly on mount.
const detail = e instanceof Error ? e.message : ''
if (!detail.includes('404')) return null
if (options.legacyFallback) {
return options.legacyFallback()
}
if (!options.legacyFallbackSource || options.filters?.archiveOnly || options.filters?.sourceIn || options.filters?.sourceNotIn) {
return null
}
const fallbackItems = await getPosts(limit, page, options.legacyFallbackSource).catch(() => null)
return fallbackItems ? buildPostListFallbackResponse(fallbackItems, options.legacyFallbackSource, limit) : null
})
}