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

@@ -29,6 +29,8 @@ import type {
} from "@/lib/api";
import Avatar from "@/components/Avatar";
import { AdText } from "@/components/AdText";
import { getPublicSettingsCached } from "@/lib/serverData";
import { announcementPath, postPath, userPath, pagePath } from "@/lib/urlStyle";
import { PulseCheckin } from "./CheckinCard";
import { FRIEND_LINKS } from "@/lib/friendLinks";
import { formatCompact, formatShortDate } from "@/lib/format";
@@ -136,7 +138,7 @@ export function PulseCard({
/* ---------- 关于本站(侧栏页目录,置于右栏最底) ---------- */
export function AboutSite({
export async function AboutSite({
pages,
siteDescription,
canManagePages,
@@ -147,6 +149,7 @@ export function AboutSite({
}) {
if (pages.length === 0 && !canManagePages) return null;
const lead = siteDescription?.trim() ?? "";
const urlStyle = (await getPublicSettingsCached()).url_style;
return (
<section className="j13-module j13-about" aria-label="关于本站">
@@ -160,7 +163,7 @@ export function AboutSite({
const Icon = pageGlyph(p.slug);
return (
<li key={p.id}>
<Link href={`/p/${p.slug}`} className="j13-about-item">
<Link href={pagePath(urlStyle, p.slug)} className="j13-about-item">
<span className="j13-about-ico" aria-hidden="true">
<Icon size={15} strokeWidth={2} />
</span>
@@ -181,13 +184,14 @@ export function AboutSite({
/* ---------- 站点公告 ---------- */
export function Announcements({
export async function Announcements({
items,
canManageAnnouncements,
}: {
items: AnnouncementSummary[];
canManageAnnouncements?: boolean;
}) {
const urlStyle = (await getPublicSettingsCached()).url_style;
return (
<section className="j13-module" aria-label="站点公告">
<div className="flex items-center justify-between gap-2">
@@ -218,7 +222,7 @@ export function Announcements({
{items.map((a) => (
<li key={a.id}>
<Link
href={`/announcement/${a.id}`}
href={announcementPath(urlStyle, a.id)}
className="j13-anno-item"
data-tip={a.title}
>
@@ -254,7 +258,8 @@ export function Announcements({
/* ---------- 热门榜 ---------- */
export function HotRanking({ posts, allowComments }: { posts: PostListItem[]; allowComments: boolean }) {
export async function HotRanking({ posts, allowComments }: { posts: PostListItem[]; allowComments: boolean }) {
const urlStyle = (await getPublicSettingsCached()).url_style;
return (
<section className="j13-module" aria-label="热门讨论">
<p className="j13-module-title mb-1.5">
@@ -266,7 +271,7 @@ export function HotRanking({ posts, allowComments }: { posts: PostListItem[]; al
<ol className="j13-ranklist">
{posts.map((p, i) => (
<li key={p.id}>
<Link href={`/post/${p.id}`} className="j13-rank" data-tip={p.title}>
<Link href={postPath(urlStyle, p.id)} className="j13-rank" data-tip={p.title}>
<span className="j13-rank-no">{i + 1}</span>
<span className="min-w-0 flex-1">
<span className="j13-rank-title truncate">
@@ -294,8 +299,9 @@ export function HotRanking({ posts, allowComments }: { posts: PostListItem[]; al
/* ---------- 最新注册 ---------- */
export function NewUsers({ users }: { users: NewUser[] }) {
export async function NewUsers({ users }: { users: NewUser[] }) {
if (users.length === 0) return null;
const urlStyle = (await getPublicSettingsCached()).url_style;
return (
<section className="j13-module" aria-label="最新注册">
<p className="j13-module-title mb-3">
@@ -307,7 +313,7 @@ export function NewUsers({ users }: { users: NewUser[] }) {
return (
<li key={u.id}>
<Link
href={`/u/${u.id}`}
href={userPath(urlStyle, u.id)}
className="flex flex-col items-center gap-1.5 rounded-lg px-1 py-1 transition-colors hover:bg-[var(--item-hover-bg)] focus-visible:bg-[var(--item-hover-bg)]"
>
<Avatar name={name} src={u.avatar} size={38} />
@@ -329,7 +335,8 @@ export function NewUsers({ users }: { users: NewUser[] }) {
/* ---------- 活跃成员(本周发帖 / 回复) ---------- */
export function ActiveUsers({ users, allowComments }: { users: ActiveUser[]; allowComments: boolean }) {
export async function ActiveUsers({ users, allowComments }: { users: ActiveUser[]; allowComments: boolean }) {
const urlStyle = (await getPublicSettingsCached()).url_style;
return (
<section className="j13-module" aria-label="本周活跃成员">
<p className="j13-module-title mb-2">
@@ -346,7 +353,7 @@ export function ActiveUsers({ users, allowComments }: { users: ActiveUser[]; all
const name = u.nickname || u.username;
return (
<li key={u.id}>
<Link href={`/u/${u.id}`} className="j13-raillink" data-tip={name}>
<Link href={userPath(urlStyle, u.id)} className="j13-raillink" data-tip={name}>
<Avatar name={name} src={u.avatar} size={30} />
<span className="flex-1 min-w-0">
<span