terry-staging #11

Merged
terry merged 37 commits from terry-staging into main 2026-05-29 19:29:58 +00:00
4 changed files with 82 additions and 2 deletions
Showing only changes of commit 6798e90708 - Show all commits

56
docs/posts-title-api.md Normal file
View File

@@ -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 |

View File

@@ -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) || ""
);
}

View File

@@ -14,6 +14,9 @@ export type PostTypeFilter = PostType | "all";
export type AttachmentKind = "image" | "video" | "document"; export type AttachmentKind = "image" | "video" | "document";
export type PostLocaleTexts = { export type PostLocaleTexts = {
/** Short display title for list/card surfaces. */
title?: string;
/** Full post body text. */
text: string; text: string;
}; };
@@ -59,6 +62,8 @@ export type Post = {
categorySlug: string; categorySlug: string;
language: string; language: string;
sourceLanguage?: string; sourceLanguage?: string;
/** Short display title for list/card surfaces. */
title?: string;
text?: string; text?: string;
localizations?: Partial<PostLocalizations>; localizations?: Partial<PostLocalizations>;
attachments: Attachment[]; attachments: Attachment[];

View File

@@ -1,6 +1,9 @@
import type { Category, Resource } from "../api"; import type { Category, Resource } from "../api";
import type { Attachment, Post } from "../types/post"; 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 & { export type PostBackedResource = Resource & {
downloadPostId?: string; downloadPostId?: string;
@@ -35,7 +38,7 @@ export function postToResource(
categories: Category[] = [], categories: Category[] = [],
): PostBackedResource { ): PostBackedResource {
const first = post.attachments[0]; 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); const category = categories.find((c) => c.id === post.categoryId);
return { return {
id: post.id, id: post.id,