feat(ssr): 帖子页全量 SSR——作者卡/目录首帧服务端渲染,消除冗余首屏请求
- 帖子快照携带作者主页资料与统计(authorProfile/authorStats), SSR 端与客户端预热均取 /api/users/:id;PostAuthorCard 首帧直接渲染, 不再挂载后请求,统计数字不再显示 — 占位 - MainLayout 首帧从已 seed 的帖子快照派生右栏 outline(作者卡+文章目录), 修复 outline 仅靠 effect 上报导致 SSR 不渲染右栏的问题 - 壳层首屏请求门控:useAuth 有种子跳过 /api/me、branding 缓存命中不重拉、 侧栏三个区块首屏用 seed 缓存、未读数首屏跳过即时请求(轮询/事件刷新保留) - serverDom(linkedom) 修复:DOMParser 片段包裹、属性顺序、SVG 自闭合展开 - 验证:首页与帖子页 hydration 后仅剩 pageview 上报,0 hydration 错误
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { getScope } from '../ssr/scope';
|
||||
|
||||
/** 构造带回跳的登录路径 */
|
||||
export function loginPath(from?: string): string {
|
||||
const path = sanitizeReturnPath(from ?? currentPath());
|
||||
@@ -36,7 +38,10 @@ export function navigateAfterAuth(
|
||||
}
|
||||
|
||||
function currentPath(): string {
|
||||
if (typeof window === 'undefined') return '/';
|
||||
if (typeof window === 'undefined') {
|
||||
// SSR:hash 不随请求上送,取请求作用域中的 pathname + search
|
||||
return getScope()?.url ?? '/';
|
||||
}
|
||||
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import type {
|
||||
SitePageSummary,
|
||||
TagCount,
|
||||
User,
|
||||
UserActivityStats,
|
||||
UserPublic,
|
||||
} from '../api/types';
|
||||
import { seedSiteBrandingCache } from '../hooks/useSiteBranding';
|
||||
import { seedForumLimitsCache } from '../hooks/useForumLimits';
|
||||
@@ -117,6 +119,9 @@ export type PostDetailSnapshot = {
|
||||
bountyCanRefund: boolean;
|
||||
bountyRefundBlockReason: string;
|
||||
bountyEligibleReplyCount: number;
|
||||
/** 作者主页资料(右栏作者卡 SSR/预热首帧直接渲染,缺省由卡片自行拉取) */
|
||||
authorProfile: UserPublic | null;
|
||||
authorStats: UserActivityStats | null;
|
||||
scrollTop: number;
|
||||
};
|
||||
|
||||
@@ -125,7 +130,11 @@ export function postDetailCacheKey(id: number): string {
|
||||
}
|
||||
|
||||
/** 由帖子详情 API 响应构建会话快照(客户端 prefetch 与服务端 +data 共用) */
|
||||
export function buildPostDetailSnapshot(detail: PostDetailResponse, comments: Comment[]): PostDetailSnapshot {
|
||||
export function buildPostDetailSnapshot(
|
||||
detail: PostDetailResponse,
|
||||
comments: Comment[],
|
||||
author?: { user: UserPublic; stats?: UserActivityStats | null } | null,
|
||||
): PostDetailSnapshot {
|
||||
return {
|
||||
post: detail.post,
|
||||
comments: Array.isArray(comments) ? comments : [],
|
||||
@@ -141,6 +150,8 @@ export function buildPostDetailSnapshot(detail: PostDetailResponse, comments: Co
|
||||
bountyCanRefund: detail.bounty_can_refund ?? true,
|
||||
bountyRefundBlockReason: detail.bounty_refund_block_reason ?? '',
|
||||
bountyEligibleReplyCount: detail.bounty_eligible_reply_count ?? 0,
|
||||
authorProfile: author?.user ?? null,
|
||||
authorStats: author?.stats ?? null,
|
||||
scrollTop: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,7 +127,11 @@ async function prefetchPost(id: number, force: boolean): Promise<void> {
|
||||
api.post(id),
|
||||
api.comments(id, myIds),
|
||||
]);
|
||||
const snap = buildPostDetailSnapshot(detail, comm.comments);
|
||||
// 作者主页资料一并预热(右栏作者卡首帧直接渲染;失败降级为卡片自行拉取)
|
||||
const author = detail.post.user_id
|
||||
? await api.userProfile(detail.post.user_id).catch(() => null)
|
||||
: null;
|
||||
const snap = buildPostDetailSnapshot(detail, comm.comments, author);
|
||||
setSessionSnapshot(key, snap);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user