From aa6ede051ed685f94439ed6782c0f03e6e65d017 Mon Sep 17 00:00:00 2001 From: k Date: Sat, 30 May 2026 03:08:59 +0800 Subject: [PATCH] feat(seo/geo): Dataset + Article + BreadcrumbList structured data, noindex archive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GEO (AI answer engines) — make the signal feeds and case studies machine-citeable: - Dataset JSON-LD on /trump, /kol, /trades (creator=Endorphin, publisher→#org, isAccessibleForFree, temporalCoverage). These are the "data/history/track record" entities Perplexity/Gemini/ChatGPT cite. - case-studies: ItemList of 5 Article nodes, each with headline/description/ articleBody/about + citation→evidence URL + author=Endorphin. ISO dates derived where parseable, omitted for vague labels. - public/llms-full.txt: single-doc full reference (methodology + glossary + case studies w/ sources inlined) for one-fetch AI ingestion; linked from llms.txt. SEO: - Breadcrumbs component (components/seo/Breadcrumbs.tsx) → BreadcrumbList JSON-LD on the 8 indexable content pages (trump/kol/macro/trades/analytics/ methodology/glossary/case-studies). - /archive split into server page + ArchivePageClient; server page sets robots noindex (legacy/test data — thin-content risk). - openGraph added to /trades and /analytics (previously meta+canonical only). Verified: tsc 0 errors, next build passes, runtime curl confirms all JSON-LD types render and llms-full.txt serves 200. No trading/API/component-logic changes. Co-Authored-By: Claude Opus 4.8 --- app/[locale]/analytics/page.tsx | 9 +- app/[locale]/archive/ArchivePageClient.tsx | 121 +++++++++++++++++++ app/[locale]/archive/page.tsx | 130 ++------------------- app/[locale]/case-studies/page.tsx | 48 ++++++++ app/[locale]/glossary/page.tsx | 2 + app/[locale]/kol/page.tsx | 39 ++++++- app/[locale]/macro/page.tsx | 12 +- app/[locale]/methodology/page.tsx | 2 + app/[locale]/trades/page.tsx | 34 +++++- app/[locale]/trump/page.tsx | 32 ++++- components/seo/Breadcrumbs.tsx | 34 ++++++ public/llms-full.txt | 111 ++++++++++++++++++ public/llms.txt | 2 + 13 files changed, 443 insertions(+), 133 deletions(-) create mode 100644 app/[locale]/archive/ArchivePageClient.tsx create mode 100644 components/seo/Breadcrumbs.tsx create mode 100644 public/llms-full.txt diff --git a/app/[locale]/analytics/page.tsx b/app/[locale]/analytics/page.tsx index 525f5ba..2012010 100644 --- a/app/[locale]/analytics/page.tsx +++ b/app/[locale]/analytics/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { getLocale } from 'next-intl/server' import AnalyticsPageClient from './AnalyticsPageClient' +import Breadcrumbs from '@/components/seo/Breadcrumbs' const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://trumpsignal.com' @@ -14,6 +15,7 @@ export async function generateMetadata(): Promise { return { title, description, + openGraph: { title, description }, alternates: { canonical: `${siteUrl}/en/analytics`, languages: { @@ -24,5 +26,10 @@ export async function generateMetadata(): Promise { } export default function AnalyticsPage() { - return + return ( + <> + + + + ) } diff --git a/app/[locale]/archive/ArchivePageClient.tsx b/app/[locale]/archive/ArchivePageClient.tsx new file mode 100644 index 0000000..0c89dd4 --- /dev/null +++ b/app/[locale]/archive/ArchivePageClient.tsx @@ -0,0 +1,121 @@ +'use client' + +import { useState, useEffect, useMemo } from 'react' +import { useLocale } from 'next-intl' +import type { TrumpPost } from '@/types' +import { getPosts } from '@/lib/api' +import PostRow from '@/components/dashboard/PostCards' +import PageHint from '@/components/ui/PageHint' +import Pagination from '@/components/ui/Pagination' + +const ARCHIVE_PAGE_SIZE = 30 + +const LIVE_SOURCES = new Set([ + 'truth', + 'btc_bottom_reversal', + 'funding_reversal', + 'kol_divergence', +]) + +/** + * Archive — legacy / test signals (rsi_reversal, sma_reclaim, breakout, + * test, phase1…). NOT a live system. Kept only so old data is inspectable. + */ +export default function ArchivePageClient() { + const locale = useLocale() + const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json + const [posts, setPosts] = useState([]) + const [loading, setLoading] = useState(true) + const [loadErr, setLoadErr] = useState('') + const [src, setSrc] = useState('all') + const [archivePage, setArchivePage] = useState(1) + + useEffect(() => { + getPosts(500, 1) + .then(p => { setPosts(p); setLoadErr('') }) + .catch(e => setLoadErr(e instanceof Error ? e.message : (isZh ? '历史归档加载失败' : 'Failed to load archive'))) + .finally(() => setLoading(false)) + }, [isZh]) + + // Archive = legacy / test data only. Exclude every live signal source so + // active modules don't leak in here as users explore old experiments. Keep + // this set in sync with sources emitted by app/services/scanners/*. + const archivePosts = useMemo( + () => posts.filter(p => !LIVE_SOURCES.has(p.source || '')), + [posts], + ) + const sources = useMemo(() => { + const m: Record = {} + for (const p of archivePosts) m[p.source || '?'] = (m[p.source || '?'] || 0) + 1 + return Object.entries(m).sort((a, b) => b[1] - a[1]) + }, [archivePosts]) + const filtered = useMemo( + () => src === 'all' ? archivePosts : archivePosts.filter(p => p.source === src), + [archivePosts, src], + ) + const archiveTotalPages = Math.max(1, Math.ceil(filtered.length / ARCHIVE_PAGE_SIZE)) + const archiveSafePage = Math.min(archivePage, archiveTotalPages) + const archivePageItems = filtered.slice((archiveSafePage - 1) * ARCHIVE_PAGE_SIZE, archiveSafePage * ARCHIVE_PAGE_SIZE) + + return ( +
+
+
+

Archive

+ + Historical fires from old scanner experiments + (rsi_reversal, sma_reclaim, breakout, test/phase1). Kept only for + inspection — the bot doesn't act on these any more. + +
+
+ +
+ {[['all', archivePosts.length] as [string, number], ...sources].map(([s, n]) => ( + + ))} +
+ + {loading &&
{isZh ? '加载中…' : 'Loading…'}
} + {!loading && loadErr && ( +
+ ⚠️ {isZh ? `无法加载归档:${loadErr}` : `Couldn't load archive — ${loadErr}`} +
+ +
+
+ )} + {!loading && !loadErr && filtered.length === 0 && ( +
+ {isZh ? '没有可显示的历史信号。' : 'No archived signals.'} +
+ )} + {!loading && archivePageItems.length > 0 && ( + <> +
+ {archivePageItems.map(p => )} +
+ + + )} +
+ ) +} diff --git a/app/[locale]/archive/page.tsx b/app/[locale]/archive/page.tsx index 49a65de..bc13b28 100644 --- a/app/[locale]/archive/page.tsx +++ b/app/[locale]/archive/page.tsx @@ -1,121 +1,13 @@ -'use client' +// Archive is legacy/test data only — not part of the live signal stack. +// noindex keeps it out of search results (duplicate/thin content risk) +// while keeping it accessible to logged-in users for inspection. +import type { Metadata } from 'next' +import ArchivePageClient from './ArchivePageClient' -import { useState, useEffect, useMemo } from 'react' -import { useLocale } from 'next-intl' -import type { TrumpPost } from '@/types' -import { getPosts } from '@/lib/api' -import PostRow from '@/components/dashboard/PostCards' -import PageHint from '@/components/ui/PageHint' -import Pagination from '@/components/ui/Pagination' - -const ARCHIVE_PAGE_SIZE = 30 - -const LIVE_SOURCES = new Set([ - 'truth', - 'btc_bottom_reversal', - 'funding_reversal', - 'kol_divergence', -]) - -/** - * Archive — legacy / test signals (rsi_reversal, sma_reclaim, breakout, - * test, phase1…). NOT a live system. Kept only so old data is inspectable. - */ -export default function ArchivePage() { - const locale = useLocale() - const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json - const [posts, setPosts] = useState([]) - const [loading, setLoading] = useState(true) - const [loadErr, setLoadErr] = useState('') - const [src, setSrc] = useState('all') - const [archivePage, setArchivePage] = useState(1) - - useEffect(() => { - getPosts(500, 1) - .then(p => { setPosts(p); setLoadErr('') }) - .catch(e => setLoadErr(e instanceof Error ? e.message : (isZh ? '历史归档加载失败' : 'Failed to load archive'))) - .finally(() => setLoading(false)) - }, [isZh]) - - // Archive = legacy / test data only. Exclude every live signal source so - // active modules don't leak in here as users explore old experiments. Keep - // this set in sync with sources emitted by app/services/scanners/*. - const archivePosts = useMemo( - () => posts.filter(p => !LIVE_SOURCES.has(p.source || '')), - [posts], - ) - const sources = useMemo(() => { - const m: Record = {} - for (const p of archivePosts) m[p.source || '?'] = (m[p.source || '?'] || 0) + 1 - return Object.entries(m).sort((a, b) => b[1] - a[1]) - }, [archivePosts]) - const filtered = useMemo( - () => src === 'all' ? archivePosts : archivePosts.filter(p => p.source === src), - [archivePosts, src], - ) - const archiveTotalPages = Math.max(1, Math.ceil(filtered.length / ARCHIVE_PAGE_SIZE)) - const archiveSafePage = Math.min(archivePage, archiveTotalPages) - const archivePageItems = filtered.slice((archiveSafePage - 1) * ARCHIVE_PAGE_SIZE, archiveSafePage * ARCHIVE_PAGE_SIZE) - - return ( -
-
-
-

Archive

- - Historical fires from old scanner experiments - (rsi_reversal, sma_reclaim, breakout, test/phase1). Kept only for - inspection — the bot doesn't act on these any more. - -
-
- -
- {[['all', archivePosts.length] as [string, number], ...sources].map(([s, n]) => ( - - ))} -
- - {loading &&
{isZh ? '加载中…' : 'Loading…'}
} - {!loading && loadErr && ( -
- ⚠️ {isZh ? `无法加载归档:${loadErr}` : `Couldn’t load archive — ${loadErr}`} -
- -
-
- )} - {!loading && !loadErr && filtered.length === 0 && ( -
- {isZh ? '没有可显示的历史信号。' : 'No archived signals.'} -
- )} - {!loading && archivePageItems.length > 0 && ( - <> -
- {archivePageItems.map(p => )} -
- - - )} -
- ) +export const metadata: Metadata = { + robots: { index: false, follow: false }, +} + +export default function ArchivePage() { + return } diff --git a/app/[locale]/case-studies/page.tsx b/app/[locale]/case-studies/page.tsx index e436609..e63ee79 100644 --- a/app/[locale]/case-studies/page.tsx +++ b/app/[locale]/case-studies/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' import Link from 'next/link' +import Breadcrumbs from '@/components/seo/Breadcrumbs' const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://trumpsignal.com' @@ -303,13 +304,60 @@ const SIGNAL_COLOR: Record = { DIVERGENCE: '#f5a524', } +// Map a free-text case date ("March 2, 2025", "April 2025", "Illustrative +// composite") to an ISO date where possible. Returns undefined for vague / +// non-date labels so we omit datePublished rather than emit garbage. +function caseIsoDate(raw: string): string | undefined { + const d = new Date(raw) + return isNaN(d.getTime()) ? undefined : d.toISOString().slice(0, 10) +} + export default async function CaseStudiesPage({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params const copy = getCopy(locale) const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json + // ── GEO: each case as a schema.org/Article in an ItemList ───────────────── + // These documented event walkthroughs are exactly what AI answer engines + // (Perplexity, Gemini, ChatGPT) cite for "did Trump posts move crypto" + // queries. Structured data makes them machine-citeable. Evidence URLs become + // citation links; Endorphin is the author/publisher. + const pageUrl = `${siteUrl}/${locale}/case-studies` + const caseJsonLd = { + '@context': 'https://schema.org', + '@type': 'ItemList', + '@id': `${pageUrl}#cases`, + name: copy.title, + description: copy.description, + itemListElement: copy.cases.map((c, i) => { + const iso = caseIsoDate(c.date) + const article: Record = { + '@type': 'Article', + '@id': `${pageUrl}#${c.id}`, + headline: c.title, + description: c.summary, + articleBody: c.detail, + about: c.asset, + author: { '@type': 'Organization', name: 'Endorphin', url: siteUrl }, + publisher: { '@id': `${siteUrl}/#org` }, + isPartOf: { '@id': `${siteUrl}/#website` }, + } + if (iso) article.datePublished = iso + if (c.externalUrl) { + article.citation = c.externalUrl + article.isBasedOn = c.externalUrl + } + return { '@type': 'ListItem', position: i + 1, item: article } + }), + } + return (
+