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

@@ -1,4 +1,5 @@
import { absoluteCanonical, resolveSiteOrigin, type QueryBag } from "./siteUrl.ts";
import { postPath, userPath, type UrlStyle } from "./urlStyle.ts";
/** 防止用户正文里的 </script> 提前闭合 JSON-LD 脚本 */
export function serializeJsonLd(data: unknown): string {
@@ -15,8 +16,8 @@ export type JsonLdPerson = {
name: string;
};
function personNode(person: JsonLdPerson) {
const url = person.id ? absoluteCanonical(`/u/${person.id}`) : undefined;
function personNode(person: JsonLdPerson, urlStyle: UrlStyle = "default") {
const url = person.id ? absoluteCanonical(userPath(urlStyle, person.id)) : undefined;
return {
"@type": "Person" as const,
name: person.name,
@@ -52,7 +53,9 @@ export function listingJsonLd(input: {
items: { id: number; name: string }[];
page: number;
pageSize: number;
urlStyle?: UrlStyle;
}): Record<string, unknown> | null {
const urlStyle = input.urlStyle ?? "default";
const site = websiteNode(input.siteName, input.siteDescription);
const pageUrl = absoluteCanonical(input.path, input.query);
const origin = resolveSiteOrigin();
@@ -75,7 +78,7 @@ export function listingJsonLd(input: {
"@type": "ListItem",
position: (input.page - 1) * input.pageSize + i + 1,
name: item.name,
url: `${origin}/post/${item.id}`,
url: `${origin}${postPath(urlStyle, item.id)}`,
})),
},
},
@@ -105,8 +108,10 @@ export function questionJsonLd(input: {
pageUrl?: string;
accepted?: JsonLdVisibleComment | null;
answers: JsonLdVisibleComment[];
urlStyle?: UrlStyle;
}): Record<string, unknown> | null {
const entityUrl = absoluteCanonical(`/post/${input.postId}`);
const urlStyle = input.urlStyle ?? "default";
const entityUrl = absoluteCanonical(postPath(urlStyle, input.postId));
const origin = resolveSiteOrigin();
const site = websiteId();
if (!entityUrl || !origin || !site) return null;
@@ -120,7 +125,7 @@ export function questionJsonLd(input: {
text: a.content,
dateCreated: a.created_at,
url: commentUrl(entityUrl, a.id),
...(a.author ? { author: personNode(a.author) } : {}),
...(a.author ? { author: personNode(a.author, urlStyle) } : {}),
});
return {
"@context": "https://schema.org",
@@ -137,7 +142,7 @@ export function questionJsonLd(input: {
...(input.text ? { text: input.text } : {}),
dateCreated: input.createdAt,
...(input.updatedAt ? { dateModified: input.updatedAt } : {}),
author: personNode(input.author),
author: personNode(input.author, urlStyle),
...(input.accepted && input.accepted.content.trim()
? { acceptedAnswer: answerNode(input.accepted) }
: {}),
@@ -158,8 +163,10 @@ export function discussionJsonLd(input: {
commentCount?: number;
viewCount?: number;
comments: JsonLdVisibleComment[];
urlStyle?: UrlStyle;
}): Record<string, unknown> | null {
const entityUrl = absoluteCanonical(`/post/${input.postId}`);
const urlStyle = input.urlStyle ?? "default";
const entityUrl = absoluteCanonical(postPath(urlStyle, input.postId));
const site = websiteId();
if (!entityUrl || !site) return null;
const pageUrl = input.pageUrl || entityUrl;
@@ -174,7 +181,7 @@ export function discussionJsonLd(input: {
datePublished: input.createdAt,
...(input.updatedAt ? { dateModified: input.updatedAt } : {}),
inLanguage: "zh-CN",
author: personNode(input.author),
author: personNode(input.author, urlStyle),
isPartOf: { "@id": site },
...(pageUrl !== entityUrl
? {
@@ -202,7 +209,7 @@ export function discussionJsonLd(input: {
text: c.content,
dateCreated: c.created_at,
url: commentUrl(entityUrl, c.id),
...(c.author ? { author: personNode(c.author) } : {}),
...(c.author ? { author: personNode(c.author, urlStyle) } : {}),
})),
}
: {}),
@@ -215,8 +222,9 @@ export function profileJsonLd(input: {
username: string;
signature?: string;
pageUrl?: string;
urlStyle?: UrlStyle;
}): Record<string, unknown> | null {
const personUrl = absoluteCanonical(`/u/${input.id}`);
const personUrl = absoluteCanonical(userPath(input.urlStyle ?? "default", input.id));
if (!personUrl) return null;
const pageUrl = input.pageUrl || personUrl;
return {