feat: Vike +data 服务端取数 / SEO Head 移植 / 404 错误页 / 服务端 DOM 模拟
- feedData.ts 聚合首页/板块/帖子详情数据,缺失内容 throw render(404) - Go 侧 spa_meta/seo 的 head 生成逻辑移植到 src/ssr/head.ts - _error 页复用站点壳渲染 404,+data 从 abortReason 恢复壳层数据 - linkedom 模拟服务端 DOM,DOMPurify 封装为 SSR 透传的 sanitizeHtml - 新增 /api/home-boot 聚合端点(handler/home_boot.go)供 SSR 冷启动灌缓存
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { User } from '../api/types';
|
||||
import { getScope } from '../ssr/scope';
|
||||
|
||||
/** 首页 SSR boot 注入的鉴权种子(consumeHomeBoot 写入;AuthProvider 仅 peek,保留至下次 boot)
|
||||
/** SSR boot 注入的鉴权种子(seedHomeBoot 写入;AuthProvider 仅 peek,保留至下次 boot)
|
||||
* SSR 时写入请求作用域,客户端为模块级单例 */
|
||||
let seededUser: User | null | undefined;
|
||||
let seededUnread = 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHtml } from './purify';
|
||||
import { POST_CONTENT_PURIFY_CONFIG } from './postContent';
|
||||
import { enhanceCodeBlocks } from './enhanceCodeBlocks';
|
||||
|
||||
@@ -64,7 +64,7 @@ function processMentionsInHtml(html: string): string {
|
||||
/** 渲染评论内容:HTML 净化 + @提及高亮 + 代码块阅读态,兼容旧版纯文本 */
|
||||
export function renderCommentContent(content: string): string {
|
||||
if (isHtmlContent(content)) {
|
||||
const sanitized = DOMPurify.sanitize(content, POST_CONTENT_PURIFY_CONFIG) as string;
|
||||
const sanitized = sanitizeHtml(content, POST_CONTENT_PURIFY_CONFIG);
|
||||
const withMentions = processMentionsInHtml(sanitized);
|
||||
const doc = new DOMParser().parseFromString(`<div id="j13-comment-root">${withMentions}</div>`, 'text/html');
|
||||
const root = doc.getElementById('j13-comment-root') ?? doc.body;
|
||||
|
||||
@@ -33,7 +33,7 @@ import { checkInCacheKey } from '../hooks/useCheckIn';
|
||||
import { isTimeDiffSignificant } from './content';
|
||||
import type { FeedSort } from '../components/FeedSortBar';
|
||||
|
||||
/** 与 Go homeBootPayload / window.__J13_HOME_BOOT__ 对齐 */
|
||||
/** 与 Go homeBootPayload 对齐(boot 数据现经 Vike +data → pageContext 传递) */
|
||||
export type HomeBootPayload = {
|
||||
board_id: number;
|
||||
sort: string;
|
||||
@@ -58,25 +58,6 @@ export type HomeBootPayload = {
|
||||
check_in?: CheckInStatus | null;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__J13_HOME_BOOT__?: HomeBootPayload;
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取并清除文档 SSR 注入的首页 boot,灌入各层缓存 */
|
||||
export function consumeHomeBoot(): HomeBootPayload | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const boot = window.__J13_HOME_BOOT__;
|
||||
try {
|
||||
delete window.__J13_HOME_BOOT__;
|
||||
} catch {
|
||||
window.__J13_HOME_BOOT__ = undefined;
|
||||
}
|
||||
if (!boot || typeof boot !== 'object') return null;
|
||||
return seedHomeBoot(boot);
|
||||
}
|
||||
|
||||
/** 将 boot 数据灌入当前缓存层(SSR 时经请求作用域隔离;客户端为模块单例) */
|
||||
export function seedHomeBoot(boot: HomeBootPayload): HomeBootPayload {
|
||||
if (boot.limits) seedForumLimitsCache(boot.limits);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import TurndownService from 'turndown';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHtml } from './purify';
|
||||
import { POST_CONTENT_PURIFY_CONFIG } from './postContent';
|
||||
import { parseFenceInfo, formatFenceInfo } from './codeBlockOptions';
|
||||
import { mapOutsideFences, wrapFencedCode } from './markdownFences';
|
||||
@@ -280,7 +280,7 @@ marked.use({
|
||||
|
||||
/** 净化并保留 members-only 标签 */
|
||||
function sanitizeContentHtml(html: string): string {
|
||||
return DOMPurify.sanitize(html, POST_CONTENT_PURIFY_CONFIG);
|
||||
return sanitizeHtml(html, POST_CONTENT_PURIFY_CONFIG);
|
||||
}
|
||||
|
||||
/** 将行首空格转为不换行空格,避免 HTML 折叠缩进;跳过围栏代码块 */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHtml } from './purify';
|
||||
import type { Config } from 'dompurify';
|
||||
import { enhanceCodeBlocks } from './enhanceCodeBlocks';
|
||||
import { enhanceHeadingAnchors } from './postHeadings';
|
||||
@@ -111,7 +111,7 @@ function extractGatedInnerHtml(el: Element, bodyClass: string, badgeClass: strin
|
||||
export function isHtmlEmpty(html: string): boolean {
|
||||
if (!html.trim()) return true;
|
||||
const doc = new DOMParser().parseFromString(
|
||||
DOMPurify.sanitize(html, POST_CONTENT_PURIFY_CONFIG) as string,
|
||||
sanitizeHtml(html, POST_CONTENT_PURIFY_CONFIG),
|
||||
'text/html',
|
||||
);
|
||||
// 有文本内容
|
||||
@@ -130,7 +130,7 @@ export function renderPostContentHtml(
|
||||
if (!html.trim()) return '';
|
||||
|
||||
const doc = new DOMParser().parseFromString(
|
||||
DOMPurify.sanitize(html, POST_CONTENT_PURIFY_CONFIG) as string,
|
||||
sanitizeHtml(html, POST_CONTENT_PURIFY_CONFIG),
|
||||
'text/html',
|
||||
);
|
||||
|
||||
|
||||
15
frontend/src/utils/purify.ts
Normal file
15
frontend/src/utils/purify.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 双端 sanitize 封装。
|
||||
*
|
||||
* - 浏览器:DOMPurify 默认实例(绑定真实 window)
|
||||
* - SSR:直接透传——内容均来自 Go API(服务端已消毒,与客户端 DOMPurify 输出一致);
|
||||
* 且 DOMPurify 的 getGlobal 只认 `window`,Node 环境拿到的默认导出没有 sanitize,
|
||||
* 注入 globalThis.window 又会翻转全项目 `typeof window` 守卫,故 SSR 走透传。
|
||||
*/
|
||||
import DOMPurify from 'dompurify';
|
||||
import type { Config } from 'dompurify';
|
||||
|
||||
export function sanitizeHtml(html: string, config?: Config): string {
|
||||
if (import.meta.env.SSR) return html;
|
||||
return DOMPurify.sanitize(html, config) as string;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { diffLines, diffWords } from 'diff';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHtml } from './purify';
|
||||
import { POST_CONTENT_PURIFY_CONFIG } from './postContent';
|
||||
|
||||
export interface PostSnapshot {
|
||||
@@ -25,7 +25,7 @@ export interface DiffPart {
|
||||
export function htmlToDiffText(html: string): string {
|
||||
if (!html.trim()) return '';
|
||||
const doc = new DOMParser().parseFromString(
|
||||
DOMPurify.sanitize(html, POST_CONTENT_PURIFY_CONFIG),
|
||||
sanitizeHtml(html, POST_CONTENT_PURIFY_CONFIG),
|
||||
'text/html',
|
||||
);
|
||||
doc.querySelectorAll('br').forEach(br => br.replaceWith('\n'));
|
||||
|
||||
Reference in New Issue
Block a user