feat: 可配置站点 URL 风格

- 新增 urlStyle 工具与测试:postPath/userPath/sitePagePath 统一生成链接
- 管理端新增 UrlStylePanel 设置面板,settings 服务与 handler 适配
- 全站路由链接与 JSON-LD/sitemap 切换为统一 URL 生成
- middleware 与相关组件适配
This commit is contained in:
2026-09-27 04:01:42 +08:00
parent 1d862a0dd3
commit 7a44c1f2cf
64 changed files with 1183 additions and 200 deletions

View File

@@ -28,6 +28,7 @@ import FollowButton from "@/components/FollowButton";
import StaffUserMenu from "@/components/StaffUserMenu";
import { requestCanonical } from "@/lib/canonicalMeta";
import { absoluteCanonical, canonicalPage } from "@/lib/siteUrl";
import { userPath } from "@/lib/urlStyle";
import { profileJsonLd } from "@/lib/jsonLd";
import JsonLd from "@/components/JsonLd";
@@ -52,7 +53,9 @@ export async function generateMetadata({ params, searchParams }: UserPageProps):
const tab = parseTab(sp.tab);
try {
const data = await fetchUserProfile(id, 1);
const allowComments = (await getPublicSettingsCached()).allow_comments !== false;
const pub = await getPublicSettingsCached();
const allowComments = pub.allow_comments !== false;
const urlStyle = pub.url_style;
const title = data.user.nickname || data.user.username;
const description = allowComments
? `查看 ${data.user.nickname} 的资料、发帖与评论`
@@ -71,7 +74,7 @@ export async function generateMetadata({ params, searchParams }: UserPageProps):
return {
title,
description,
...(await requestCanonical(`/u/${data.user.id}`, query)),
...(await requestCanonical(userPath(urlStyle, data.user.id), query)),
};
} catch {
return { title: "用户不存在" };
@@ -93,6 +96,7 @@ export default async function UserPage({ params, searchParams }: UserPageProps)
const me = await getMeCached(cookie || undefined);
const settings = await getPublicSettingsCached();
const allowComments = settings.allow_comments !== false;
const urlStyle = settings.url_style;
const commentsRequireLogin = settings.comments_require_login === true;
const allowMessages = settings.allow_messages !== false;
const canReadComments = allowComments && (!commentsRequireLogin || !!me.user);
@@ -206,7 +210,8 @@ export default async function UserPage({ params, searchParams }: UserPageProps)
]
: [];
const visibleNav = [...mainNavItems, ...settingsNavItems];
const tabHref = (t: UserTab) => (t === "posts" ? `/u/${id}` : `/u/${id}?tab=${t}`);
const tabHref = (t: UserTab) =>
t === "posts" ? userPath(urlStyle, Number(id)) : `${userPath(urlStyle, Number(id))}?tab=${t}`;
const profileQuery: Record<string, string | number> = {};
if (canReadComments && effectiveTab === "comments") profileQuery.tab = "comments";
@@ -222,7 +227,8 @@ export default async function UserPage({ params, searchParams }: UserPageProps)
name: u.nickname || u.username,
username: u.username,
signature: (u.signature || "").trim() || undefined,
pageUrl: absoluteCanonical(`/u/${u.id}`, profileQuery),
pageUrl: absoluteCanonical(userPath(urlStyle, u.id), profileQuery),
urlStyle,
});
/** 本页卡片统一阴影;暗色随 color-mix 自动加深 */
@@ -255,12 +261,12 @@ export default async function UserPage({ params, searchParams }: UserPageProps)
{/* 本人操作按钮 / 访客私聊:绝对定位在卡片右上角 */}
{isOwner ? (
<div className="absolute top-3 right-3 z-20 flex items-center gap-2">
<Link href={`/u/${id}?tab=settings`} className="btn btn-line btn-sm">
<Link href={`${userPath(urlStyle, Number(id))}?tab=settings`} className="btn btn-line btn-sm">
<PenLine size={13.5} />
<span className="hidden sm:inline">编辑资料</span>
</Link>
<Link
href={`/u/${id}?tab=security`}
href={`${userPath(urlStyle, Number(id))}?tab=security`}
aria-label="安全设置"
data-tip="安全设置"
className="inline-flex w-8 h-8 items-center justify-center rounded-full border transition-colors duration-150 hover:border-[var(--accent)] hover:text-[var(--accent)]"