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:
+118
@@ -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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user