feat: 轻量发文 posts 模块(编辑器直发 + /feed 原创 Tab + 个人主页 + 详情页 + 转正 PR)#350
Merged
Conversation
新增 PostContent UGC Markdown 渲染器(react-markdown + rehype-sanitize,XSS 防护), EditorPageClient 改造为直发 POST /api/posts,/feed 加原创文章默认 Tab, /u/[username]/posts 列表页和 /u/[username]/posts/[slug] 详情页, PromoteToDocsButton 三态按钮支持一键转正 PR,个人主页追加文章入口。
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new “轻量发文” posts feature to the Next.js app so users can publish Markdown directly from /editor to the backend (no Git PR required), browse posts via a new /feed “原创文章” tab, and view user post lists/detail pages under /u/{identifier}/posts.
Changes:
- Introduces posts typing + UI components (UGC Markdown renderer, post cards, profile entry link, promote-to-docs action).
- Updates
/editorpublish flow toPOST /api/postsand navigates to the new post detail route on success. - Extends routing/infra:
/api/posts*rewrites,/feedtop-level tab switcher, and robots rules to keep UGC out of indexing; adds developer docs.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks new dependencies for runtime Markdown rendering/sanitization. |
| package.json | Adds react-markdown + rehype-sanitize dependencies. |
| next.config.mjs | Adds /api/posts and /api/posts/:path* rewrites to backend. |
| dev_docs/posts_lightweight_publish.md | Documents posts lightweight publish flow, routes, and auth header conventions. |
| app/types/post.ts | Adds frontend DTO typings aligned with backend posts module. |
| app/robots.ts | Disallows indexing of /u/*/posts/ for UGC. |
| app/components/PromoteToDocsButton.tsx | Adds promote-to-docs 3-state button and promote API call. |
| app/components/PostContent.tsx | Adds UGC Markdown renderer with rehype pipeline + sanitization. |
| app/[locale]/u/[username]/PostsLinkOnProfile.tsx | Adds profile entry link to posts list and (owner-only) count fetch. |
| app/[locale]/u/[username]/posts/page.tsx | Adds client-rendered user posts list page with owner/public behavior. |
| app/[locale]/u/[username]/posts/layout.tsx | Adds layout wrapper to keep Header/Footer server-rendered. |
| app/[locale]/u/[username]/posts/[slug]/PostDetailOwnerActions.tsx | Adds owner-only actions (delete + promote). |
| app/[locale]/u/[username]/posts/[slug]/page.tsx | Adds SSR post detail page with noindex metadata and PostContent rendering. |
| app/[locale]/u/[username]/page.tsx | Wires profile page to show posts entry section. |
| app/[locale]/feed/page.tsx | Adds “posts vs links” tab behavior and SSR fetching for posts feed. |
| app/[locale]/feed/components/PostCard.tsx | Adds post summary card used by feed and user posts list. |
| app/[locale]/feed/components/FeedTabSwitcher.tsx | Adds client tab switcher controlling `?tab=posts |
| app/[locale]/editor/EditorPageClient.tsx | Switches editor publish from GitHub PR flow to direct posts API publish. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| attributes: { | ||
| ...defaultSchema.attributes, | ||
| // 允许所有元素携带 className(rehype-katex / rehype-autolink-headings 需要) | ||
| "*": [...(defaultSchema.attributes?.["*"] ?? []), "className", "style"], |
Comment on lines
+4
to
+7
| import { DocsDestinationForm } from "@/app/components/DocsDestinationForm"; | ||
| import { buildDocsNewUrl } from "@/lib/github"; | ||
| import { buildFrontmatter } from "@/app/[locale]/editor/EditorPageClient"; | ||
|
|
Comment on lines
+55
to
+66
| const token = localStorage.getItem("satoken") ?? ""; | ||
| const res = await fetch(`/api/posts/${postId}`, { | ||
| method: "DELETE", | ||
| // rewrite 透传:后端读 satoken,不是 x-satoken | ||
| headers: { satoken: token }, | ||
| }); | ||
| const body = (await res.json()) as ApiResponse<void>; | ||
| if (res.ok && body.success) { | ||
| router.replace(`/u/${authorUsername}/posts`); | ||
| } else { | ||
| alert(body.message ?? `删除失败(HTTP ${res.status})`); | ||
| } |
Comment on lines
+25
to
+28
| /** | ||
| * 从文章标题生成 slug 候选值,和后端生成逻辑保持一致(kebab-case,纯 ASCII)。 | ||
| * 后端会做唯一性去重,前端只是提前填充 filename input 用,不是最终 slug。 | ||
| */ |
Comment on lines
+203
to
+210
| const postRequest: PostRequest = { | ||
| title: title.trim(), | ||
| description: description.trim() || undefined, | ||
| tags: tags.filter((t) => t.trim().length > 0), | ||
| contentMd: finalMarkdown, | ||
| // 有用户填的 slug 就带上,后端会去重;没有则不传,后端从 title 自动生成 | ||
| ...(rawSlug ? { slug: rawSlug } : {}), | ||
| }; |
Comment on lines
+212
to
+221
| const res = await fetch("/api/posts", { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| // rewrite 透传:后端 sa-token.token-name=satoken,需用 satoken 而非 x-satoken | ||
| satoken: token, | ||
| }, | ||
| body: JSON.stringify(postRequest), | ||
| signal: AbortSignal.timeout(30_000), | ||
| }); |
Comment on lines
+112
to
+121
| const token = localStorage.getItem("satoken") ?? ""; | ||
| fetch(`/api/posts/${postId}/promote`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| // rewrite 透传:后端读 satoken,不是 x-satoken | ||
| satoken: token, | ||
| }, | ||
| body: JSON.stringify({ prUrl: githubUrl }), | ||
| }).catch((err) => { |
- 卡片 003「笔试面经」href: /docs/career/interview-prep/bq → /docs/career(死链修复) - 卡片 004「群友分享」href: /docs/community → /feed?tab=posts(对接 posts 功能) - Hero 右侧 CTA feed href: /feed → /feed?tab=posts - zh CTA 文案「看看我们最近在读什么」→「看看群友在写什么」(与卡片标题「群友分享」区分) - en CTA 文案 → "See what we're writing";community.desc 补全为 "Original posts from community members" - zh community.desc「群友写的捏」→「群友原创文章,不定期更新」
- 003「笔试面经」href 恢复 /docs/career/interview-prep/bq(上次误改成 /docs/career 回滚) - 004 标题「群友分享」→「群友创作」(与 feed 分享链接 Tab 区分) - zh CTA「看看群友在写什么」→「去看群友在写什么 →」 - en CTA → "See what the community is writing →" - en community.title → "Community Creations" - 001/002/003 href 全部不动
- zh desc「群友原创文章,不定期更新」→「群友随手写的文章,不用等 PR review,发完即在。选题自由,从踩坑记录到技术思考都有。」 - en desc 同步更新(期待感文案,说明发完即在、选题自由) - title/href/CTA 在上一 commit 已到位,本次仅补 desc
- title: "Community Creations" → "Community Posts" - desc: 措辞对齐终版(published instantly, dev notes to technical deep-dives)
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
改动概述
新增「轻量发文」功能:用户在
/editor写 Markdown 后直接POST /api/posts落后端 DB,无需 Git PR。后端 PR:关联
feat/posts-module(Spring Boot posts 模块,含 posts 表、接口、SaToken 公开白名单)前端改动清单
新增组件
app/types/post.ts— PostView / PostSummaryView / PostRequest 类型(与后端 DTO 对齐)app/components/PostContent.tsx— UGC Markdown 渲染(react-markdown + rehype-sanitize,XSS 防护)app/components/PromoteToDocsButton.tsx— 三态转正按钮(idle / pending / promoted,不可逆状态机)app/[locale]/feed/components/FeedTabSwitcher.tsx— 原创文章 / 分享链接 Tab 切换app/[locale]/feed/components/PostCard.tsx— 文章卡片(showAuthor prop,反色角标)app/[locale]/u/[username]/PostsLinkOnProfile.tsx— 个人主页文章入口计数app/[locale]/u/[username]/posts/— 文章列表页(client)+ 详情页(SSR)+ layout改动文件
EditorPageClient.tsx— 移除 DocsDestinationForm,改为 POST /api/posts 直发,成功后跳详情页feed/page.tsx— 加 posts Tab(默认),fetchPosts() 三次退避策略u/[username]/page.tsx— 追加 PostsLinkOnProfile 文章入口next.config.mjs— 新增 /api/posts rewrite 规则(透传到后端)robots.ts— disallow /u/*/posts/(UGC noindex)Auth Header 约定
所有
/api/posts*fetch 用satoken: token(rewrite 透传,与 /feed/submit 的 /api/community/links 一致)。/api/upload仍用x-satoken(Next API Route,内部转换)。Build 路由表
新增两条 ƒ Dynamic 路由(符合预期,无已有路由被意外翻成 ƒ):
ƒ /[locale]/u/[username]/posts— client 组件,读 localStorage 判 ownerƒ /[locale]/u/[username]/posts/[slug]— SSR,cache: no-store上线前提
后端
feat/posts-module必须同步上线,且 SaTokenConfigure 公开读白名单包含:GET /api/posts/feedGET /api/posts/*/*否则匿名用户访问 /feed 原创 Tab 和详情页会 401。
测试
tester 全轮验收通过(用真实 GitHub OAuth token 测发布主流程)。
开发者文档
见
dev_docs/posts_lightweight_publish.md。