diff --git a/docs/posts-title-api.md b/docs/posts-title-api.md new file mode 100644 index 0000000..1522836 --- /dev/null +++ b/docs/posts-title-api.md @@ -0,0 +1,56 @@ +# Posts title fields for list/card surfaces + +Front-end list/card surfaces such as **热门资料**, **官方推荐**, and **全部资料** should display a short title, not the full Telegram/body text. + +## Affected endpoints + +Any endpoint returning `Post` items should include a short title when available: + +- `GET /api/posts` +- `GET /api/posts/search` +- `GET /api/posts/recommended` +- `GET /api/posts/:id` + +## Recommended response shape + +```jsonc +{ + "id": "string", + "title": "ARK 2026 共识加速计划", // optional global fallback title + "text": "完整正文 / Telegram-style body text...", + "localizations": { + "zh": { + "title": "ARK 2026 共识加速计划", + "text": "完整中文正文...", + }, + "en": { + "title": "ARK 2026 Consensus Acceleration Plan", + "text": "Full English body...", + }, + }, +} +``` + +## Front-end fallback order + +For `Resource.title`, front-end reads: + +1. `localizations[currentLang].title` +2. `post.title` +3. first non-empty line of localized/full `text` +4. first attachment filename +5. `post.id` + +So backend can roll this out gradually: old posts without `title` still render, but long body text will be reduced to its first line. + +## Requirement + +Do **not** put an entire body paragraph into `title`. `title` should be concise enough for a two-line card/list title. + +Examples: + +| Good title | Bad title | +| ------------------------------------ | --------------------------------------------------------- | +| `ARK 2026「共识加速计划」邀请王霸榜` | Full event body with links, schedule, rules, and hashtags | +| `ARK 主网核心合约地址(BSC链)` | Full contract explainer paragraph | +| `ARK灵魂五问完整视频` | Full video caption text | diff --git a/src/components/messageStream/utils/postText.ts b/src/components/messageStream/utils/postText.ts index 2ba5bd2..b469613 100644 --- a/src/components/messageStream/utils/postText.ts +++ b/src/components/messageStream/utils/postText.ts @@ -11,3 +11,19 @@ export function postDisplayText(post: Post, lang: string): string { "" ); } + +export function postTitleText(post: Post, lang: string): string { + const key = localizationKey(lang); + const localized = + post.localizations?.[key as keyof typeof post.localizations]; + const explicitTitle = localized?.title?.trim() || post.title?.trim(); + if (explicitTitle) return explicitTitle; + + const text = postDisplayText(post, lang); + return ( + text + .split(/\n{1,}/) + .map((line) => line.trim()) + .find(Boolean) || "" + ); +} diff --git a/src/types/post.ts b/src/types/post.ts index 14d94bc..4ffe78b 100644 --- a/src/types/post.ts +++ b/src/types/post.ts @@ -14,6 +14,9 @@ export type PostTypeFilter = PostType | "all"; export type AttachmentKind = "image" | "video" | "document"; export type PostLocaleTexts = { + /** Short display title for list/card surfaces. */ + title?: string; + /** Full post body text. */ text: string; }; @@ -59,6 +62,8 @@ export type Post = { categorySlug: string; language: string; sourceLanguage?: string; + /** Short display title for list/card surfaces. */ + title?: string; text?: string; localizations?: Partial; attachments: Attachment[]; diff --git a/src/utils/postResourceAdapter.ts b/src/utils/postResourceAdapter.ts index fec65b3..aa850d0 100644 --- a/src/utils/postResourceAdapter.ts +++ b/src/utils/postResourceAdapter.ts @@ -1,6 +1,9 @@ import type { Category, Resource } from "../api"; import type { Attachment, Post } from "../types/post"; -import { postDisplayText } from "../components/messageStream/utils/postText"; +import { + postDisplayText, + postTitleText, +} from "../components/messageStream/utils/postText"; export type PostBackedResource = Resource & { downloadPostId?: string; @@ -35,7 +38,7 @@ export function postToResource( categories: Category[] = [], ): PostBackedResource { const first = post.attachments[0]; - const title = postDisplayText(post, lang) || first?.filename || post.id; + const title = postTitleText(post, lang) || first?.filename || post.id; const category = categories.find((c) => c.id === post.categoryId); return { id: post.id,