公开页补齐自引用 Canonical 与按类型的结构化数据,图片补上说明文字。
后台设置和内容编辑改成可导航布局;部署用访客 origin 生成 Canonical,避免落到容器地址。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -43,6 +43,10 @@ import { canModerateBoard } from "@/lib/roles";
|
||||
import { IconComment, IconEye } from "@/components/Icons";
|
||||
import { normalizePostType, postTypeLabel, contentAccessLabel, typeStatusLabel, typeStatusClass } from "@/lib/postMeta";
|
||||
import { extractToc, estimateReadingMinutes, markdownHeadingToc } from "@/lib/toc";
|
||||
import { requestCanonical } from "@/lib/canonicalMeta";
|
||||
import { absoluteCanonical, canonicalPage } from "@/lib/siteUrl";
|
||||
import { discussionJsonLd, questionJsonLd, type JsonLdVisibleComment } from "@/lib/jsonLd";
|
||||
import JsonLd from "@/components/JsonLd";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -50,8 +54,9 @@ interface PageProps {
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
export async function generateMetadata({ params, searchParams }: PageProps): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
const sp = await searchParams;
|
||||
try {
|
||||
// 带登录态:作者/管理团队看待审帖时标签标题也应是真实标题而非"帖子不存在"
|
||||
const { post } = await fetchPostDetail(id, postViewerCookieHeader(await cookies()));
|
||||
@@ -66,6 +71,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
||||
const desc = publicBody
|
||||
? publicBody.slice(0, 160)
|
||||
: post.access_hint || "部分内容需满足条件后可见";
|
||||
const page = canonicalPage(sp.page);
|
||||
return {
|
||||
title: post.title,
|
||||
description: desc,
|
||||
@@ -74,6 +80,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
||||
description: desc,
|
||||
type: "article",
|
||||
},
|
||||
...(await requestCanonical(`/post/${post.id}`, page ? { page } : undefined)),
|
||||
};
|
||||
} catch {
|
||||
return { title: "帖子不存在" };
|
||||
@@ -505,39 +512,63 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
}
|
||||
: null;
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "DiscussionForumPosting",
|
||||
headline: post.title,
|
||||
// 仅公开片段进 JSON-LD,避免 SEO 泄露隐藏块原文
|
||||
articleBody: publicBody || undefined,
|
||||
datePublished: post.created_at,
|
||||
dateModified: post.updated_at,
|
||||
author: { "@type": "Person", name: post.user.nickname },
|
||||
...(allowComments ? { commentCount: post.comment_count } : {}),
|
||||
interactionStatistic: {
|
||||
"@type": "InteractionCounter",
|
||||
interactionType: { "@type": "ReadAction" },
|
||||
userInteractionCount: post.view_count,
|
||||
},
|
||||
...(allowComments &&
|
||||
acceptedAnswer &&
|
||||
!acceptedAnswer.deleted &&
|
||||
!!publicBody && {
|
||||
acceptedAnswer: {
|
||||
"@type": "Comment",
|
||||
text: acceptedAnswer.content,
|
||||
dateCreated: acceptedAnswer.created_at,
|
||||
const entityUrl = absoluteCanonical(`/post/${post.id}`);
|
||||
const pageUrl =
|
||||
absoluteCanonical(`/post/${post.id}`, commentPage > 1 ? { page: commentPage } : undefined) ||
|
||||
entityUrl;
|
||||
const authorName = post.user.nickname || post.user.username;
|
||||
const visibleFloors: JsonLdVisibleComment[] = allowComments
|
||||
? comments
|
||||
.filter((c) => !c.deleted && (c.content || "").trim())
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
content: c.content,
|
||||
created_at: c.created_at,
|
||||
author: c.user
|
||||
? { id: c.user.id, name: c.user.nickname || c.user.username }
|
||||
: undefined,
|
||||
}))
|
||||
: [];
|
||||
const acceptedForLd: JsonLdVisibleComment | null =
|
||||
allowComments && acceptedAnswer && !acceptedAnswer.deleted && acceptedAnswer.content?.trim()
|
||||
? {
|
||||
id: acceptedAnswer.id,
|
||||
content: acceptedAnswer.content,
|
||||
created_at: acceptedAnswer.created_at,
|
||||
author: acceptedAnswer.user
|
||||
? {
|
||||
"@type": "Person",
|
||||
id: acceptedAnswer.user.id,
|
||||
name: acceptedAnswer.user.nickname || acceptedAnswer.user.username,
|
||||
}
|
||||
: undefined,
|
||||
url: `#comment-${acceptedAnswer.id}`,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
: null;
|
||||
// 问答帖才是「一问多答」;悬赏/投票仍是讨论帖,不套 QAPage,也不把采纳标成 Question.acceptedAnswer
|
||||
const jsonLd =
|
||||
postType === "question"
|
||||
? questionJsonLd({
|
||||
title: post.title,
|
||||
text: publicBody || undefined,
|
||||
createdAt: post.created_at,
|
||||
updatedAt: edited ? post.updated_at : undefined,
|
||||
author: { id: post.user.id, name: authorName },
|
||||
postId: post.id,
|
||||
pageUrl,
|
||||
accepted: acceptedForLd,
|
||||
answers: visibleFloors,
|
||||
})
|
||||
: discussionJsonLd({
|
||||
title: post.title,
|
||||
text: publicBody || undefined,
|
||||
createdAt: post.created_at,
|
||||
updatedAt: edited ? post.updated_at : undefined,
|
||||
author: { id: post.user.id, name: authorName },
|
||||
postId: post.id,
|
||||
pageUrl,
|
||||
commentCount: allowComments ? post.comment_count : undefined,
|
||||
viewCount: post.view_count,
|
||||
comments: visibleFloors,
|
||||
});
|
||||
|
||||
// 仅正文 Markdown 标题与 headingRenderer 按序对应(时间线日锚点另有 tl-* id)
|
||||
let headingIndex = 0;
|
||||
@@ -557,7 +588,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
|
||||
return (
|
||||
<>
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||
<JsonLd data={jsonLd} />
|
||||
{/* 移动端浮动操作条:长文读到中途可点赞 / 跳评论框 */}
|
||||
<MobilePostBar
|
||||
postId={id}
|
||||
@@ -678,8 +709,13 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
</span>
|
||||
</div>
|
||||
<p className="meta w-full text-[12px] sm:w-auto sm:ml-auto">
|
||||
发布于 {formatDateTime(post.created_at)}
|
||||
{edited && <> · 更新于 {formatDateTime(post.updated_at)}</>}
|
||||
发布于 <time dateTime={post.created_at}>{formatDateTime(post.created_at)}</time>
|
||||
{edited && (
|
||||
<>
|
||||
{" "}
|
||||
· 更新于 <time dateTime={post.updated_at}>{formatDateTime(post.updated_at)}</time>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user