feat(posts): support short titles for resource cards
Add optional post-level and localized title fields, and use a new postTitleText helper for Resource.title so card/list surfaces prefer short backend-provided titles instead of full body text. When title is missing, fallback to the first non-empty body line, then filename, then post id. Document the backend handoff in docs/posts-title-api.md alongside the other backend task docs.
This commit is contained in:
56
docs/posts-title-api.md
Normal file
56
docs/posts-title-api.md
Normal 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 |
|
||||||
@@ -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) || ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user