Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 10 additions & 48 deletions app/[locale]/editor/EditorPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ interface EditorPageClientProps {
user: UserView;
}

/**
* 从文章标题生成 slug 候选值,和后端生成逻辑保持一致(kebab-case,纯 ASCII)。
* 后端会做唯一性去重,前端只是提前填充 filename input 用,不是最终 slug。
*/
// titleToSlug:从文章标题生成 slug 候选值,供前端预填 filename input。
// 保留 Unicode 字母/数字(\p{L}\p{N}),允许中文 slug 候选,和后端 sanitizeSlug 对齐。
// 后端会做唯一性去重,前端候选值不是最终 slug。
function titleToSlug(title: string): string {
return title
.toLowerCase()
Expand All @@ -37,44 +36,6 @@ function titleToSlug(title: string): string {
.slice(0, 128);
}

// buildFrontmatter 仅在「收录进知识库」(PromoteToDocsButton)路径使用,
// 这里为 PromoteToDocsButton 单独导出,editor 直发不再拼 frontmatter。
export function buildFrontmatter({
title,
description,
tags,
}: {
title: string;
description?: string;
tags?: string[];
}) {
const safeTitle = JSON.stringify(title);
const safeDescription = JSON.stringify(description ?? "");
const date = new Date().toISOString().slice(0, 10);
const normalizedTags = (tags ?? [])
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);

const lines = [
"---",
`title: ${safeTitle}`,
`description: ${safeDescription}`,
`date: "${date}"`,
];

if (normalizedTags.length > 0) {
lines.push(
"tags:",
...normalizedTags.map((tag) => ` - ${JSON.stringify(tag)}`),
);
} else {
lines.push("tags: []");
}

lines.push("---");
return lines.join("\n");
}

export function EditorPageClient({ user }: EditorPageClientProps) {
const router = useRouter();
const [isPublishing, setIsPublishing] = useState(false);
Expand Down Expand Up @@ -195,15 +156,16 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
});
}

// POST /api/posts 直发落库
// TODO(backend-contract): 等后端 #2 完成后确认 BACKEND_URL 路由 rewrite 情况;
// 目前后端路由走 next.config.mjs rewrites 同源代理(参考 /api/community/links),
// 若 posts 同样走代理则直接 fetch "/api/posts",否则需要带 BACKEND_URL。
const token = localStorage.getItem("satoken") ?? "";
if (!token) {
throw new Error("请先登录后再发布");
}

const postRequest: PostRequest = {
title: title.trim(),
description: description.trim() || undefined,
tags: tags.filter((t) => t.trim().length > 0),
// trim + filter 保证后端收到的 tags 无空白项
tags: tags.map((t) => t.trim()).filter(Boolean),
contentMd: finalMarkdown,
// 有用户填的 slug 就带上,后端会去重;没有则不传,后端从 title 自动生成
...(rawSlug ? { slug: rawSlug } : {}),
Expand All @@ -214,7 +176,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
headers: {
"Content-Type": "application/json",
// rewrite 透传:后端 sa-token.token-name=satoken,需用 satoken 而非 x-satoken
satoken: token,
...(token ? { satoken: token } : {}),
},
body: JSON.stringify(postRequest),
signal: AbortSignal.timeout(30_000),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ export function PostDetailOwnerActions({
const token = localStorage.getItem("satoken") ?? "";
const res = await fetch(`/api/posts/${postId}`, {
method: "DELETE",
// rewrite 透传:后端读 satoken,不是 x-satoken
headers: { satoken: token },
// rewrite 透传:后端读 satoken,不是 x-satoken;空 token 不发 header
headers: { ...(token ? { satoken: token } : {}) },
});
const body = (await res.json()) as ApiResponse<void>;
const body = (await res.json().catch(() => ({}))) as ApiResponse<void>;
if (res.ok && body.success) {
router.replace(`/u/${authorUsername}/posts`);
Comment on lines +59 to 63
} else {
Expand Down
8 changes: 6 additions & 2 deletions app/components/PostContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@ const sanitizeSchema = {
],
attributes: {
...defaultSchema.attributes,
// 允许所有元素携带 className(rehype-katex / rehype-autolink-headings 需要)
"*": [...(defaultSchema.attributes?.["*"] ?? []), "className", "style"],
// className 全局允许(rehype-katex / rehype-autolink-headings 均需要)
// style 不全局放行(XSS via CSS),只给 KaTeX 渲染必须的元素开放
"*": [...(defaultSchema.attributes?.["*"] ?? []), "className"],
// KaTeX span/svg 需要 style 控制数学符号排版,普通 UGC 元素不需要
span: [...(defaultSchema.attributes?.["span"] ?? []), "style"],
svg: [...(defaultSchema.attributes?.["svg"] ?? []), "style"],
// KaTeX math 元素的专有属性
math: ["xmlns", "display"],
annotation: ["encoding"],
Expand Down
6 changes: 3 additions & 3 deletions app/components/PromoteToDocsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useState } from "react";
import { DocsDestinationForm } from "@/app/components/DocsDestinationForm";
import { buildDocsNewUrl } from "@/lib/github";
import { buildFrontmatter } from "@/app/[locale]/editor/EditorPageClient";
import { buildFrontmatter } from "@/lib/frontmatter";

interface Props {
postId: number;
Expand Down Expand Up @@ -114,8 +114,8 @@ export function PromoteToDocsButton({
method: "POST",
headers: {
"Content-Type": "application/json",
// rewrite 透传:后端读 satoken,不是 x-satoken
satoken: token,
// rewrite 透传:后端读 satoken,不是 x-satoken;空 token 不发 header
...(token ? { satoken: token } : {}),
Comment on lines 114 to +118
},
body: JSON.stringify({ prUrl: githubUrl }),
}).catch((err) => {
Expand Down
41 changes: 41 additions & 0 deletions lib/frontmatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* buildFrontmatter:为 /docs 知识库生成 YAML frontmatter 字符串。
*
* 抽到独立模块的原因:EditorPageClient 和 PromoteToDocsButton 都需要它,
* 把它留在 EditorPageClient.tsx 会让详情页/卡片 bundle 拖进整个编辑器栈。
Comment on lines +4 to +5
*/
export function buildFrontmatter({
title,
description,
tags,
}: {
title: string;
description?: string;
tags?: string[];
}): string {
const safeTitle = JSON.stringify(title);
const safeDescription = JSON.stringify(description ?? "");
const date = new Date().toISOString().slice(0, 10);
const normalizedTags = (tags ?? [])
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);

const lines = [
"---",
`title: ${safeTitle}`,
`description: ${safeDescription}`,
`date: "${date}"`,
];

if (normalizedTags.length > 0) {
lines.push(
"tags:",
...normalizedTags.map((tag) => ` - ${JSON.stringify(tag)}`),
);
} else {
lines.push("tags: []");
}

lines.push("---");
return lines.join("\n");
}
Loading