179 lines
7.0 KiB
TypeScript
179 lines
7.0 KiB
TypeScript
import type { Metadata } from "next";
|
||
import { cookies, headers } from "next/headers";
|
||
import { Plus_Jakarta_Sans, Noto_Sans_SC, JetBrains_Mono } from "next/font/google";
|
||
import "./globals.css";
|
||
import SiteBrandProvider from "@/components/SiteBrandProvider";
|
||
import SiteChrome from "@/components/SiteChrome";
|
||
import Toaster from "@/components/Toaster";
|
||
import TipHost from "@/components/TipHost";
|
||
import AccountGuard from "@/components/AccountGuard";
|
||
import ThemeSync from "@/components/ThemeSync";
|
||
import RealtimeProvider from "@/components/RealtimeProvider";
|
||
import VisitBeacon from "@/components/VisitBeacon";
|
||
import PostLinkProvider from "@/components/PostLinkProvider";
|
||
import CodeFoldProvider from "@/components/markdown/CodeFoldProvider";
|
||
import MotionProvider from "@/components/MotionProvider";
|
||
import CommentsPolicyProvider from "@/components/CommentsPolicyProvider";
|
||
import MessagesPolicyProvider from "@/components/MessagesPolicyProvider";
|
||
import type { User } from "@/lib/api";
|
||
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
|
||
import { TOKEN_COOKIE, authCookieHeader } from "@/lib/cookies";
|
||
import { accentStyleCSS } from "@/lib/theme";
|
||
import { bgConfigFromSettings, bgStyleCSS } from "@/lib/bg";
|
||
import { siteDocumentTitle } from "@/lib/brand";
|
||
import { motionConfigFromSettings } from "@/lib/motion";
|
||
|
||
// 构建期下载并自托管字体:同源加载 + 自动 preload + fallback 度量校正,
|
||
// 消除 F5 时 Google CDN 字体到达后整页"由粗变细"的 FOUT 闪动
|
||
const jakarta = Plus_Jakarta_Sans({
|
||
subsets: ["latin"],
|
||
weight: ["400", "500", "600", "700", "800"],
|
||
variable: "--font-jakarta",
|
||
display: "swap",
|
||
});
|
||
|
||
const notoSC = Noto_Sans_SC({
|
||
// 须覆盖站内实际用到的字重:缺档时浏览器会伪粗(synthetic bold),中文在 2K 屏发虚发糊
|
||
weight: ["400", "500", "600", "700", "800", "900"],
|
||
variable: "--font-noto-sc",
|
||
display: "swap",
|
||
});
|
||
|
||
const jetbrainsMono = JetBrains_Mono({
|
||
subsets: ["latin"],
|
||
weight: ["400", "500", "700"],
|
||
variable: "--font-jetbrains",
|
||
display: "swap",
|
||
});
|
||
|
||
export async function generateMetadata(): Promise<Metadata> {
|
||
const settings = await getPublicSettingsCached();
|
||
const name = settings.site_name || "姜十三论坛";
|
||
const title = siteDocumentTitle(name, settings.site_slogan || "");
|
||
const desc = settings.site_description || "姜十三论坛 - 技术分享与讨论社区";
|
||
const keywords = settings.site_keywords?.length ? settings.site_keywords : undefined;
|
||
return {
|
||
title: {
|
||
default: title,
|
||
template: `%s · ${name}`,
|
||
},
|
||
description: desc,
|
||
keywords,
|
||
...(settings.favicon_url
|
||
? { icons: { icon: settings.favicon_url } }
|
||
: {}),
|
||
openGraph: {
|
||
title,
|
||
description: desc,
|
||
type: "website",
|
||
},
|
||
};
|
||
}
|
||
|
||
export default async function RootLayout({
|
||
children,
|
||
}: {
|
||
children: React.ReactNode;
|
||
}) {
|
||
const cookieStore = await cookies();
|
||
const headerStore = await headers();
|
||
// middleware 写入 x-pathname;供 SiteChrome SSR 初值(软导航靠客户端 pathname)
|
||
const pathname = headerStore.get("x-pathname") || "";
|
||
const isAdmin = pathname === "/admin" || pathname.startsWith("/admin/");
|
||
|
||
// 浅色优先的设计系统,仅在显式选择暗色时挂 .dark
|
||
const theme = cookieStore.get("j13-theme")?.value === "dark" ? "dark" : "light";
|
||
|
||
// SSR 直出登录态(access 过期时 middleware 已先行轮转并注入请求头,见 middleware.ts),
|
||
// 保证 F5 初始 HTML 中的右上角就是正确的用户区,而非先闪"登录/注册"。
|
||
// 无 access cookie 即按游客渲染;客户端 Header 在 CSRF 可读时再静默校正一次。
|
||
// 站点主题色(公开设置):派生浅/暗两套 accent CSS 变量后随首屏 HTML 内联,
|
||
// 覆盖 globals.css 默认令牌——无主题闪动(FOUC),后端不可用时降级为默认靛蓝
|
||
const settings = await getPublicSettingsCached();
|
||
const accent = settings.accent;
|
||
const accentCSS = accentStyleCSS(accent || "");
|
||
const bgCfg = bgConfigFromSettings(settings);
|
||
const bgCSS = bgStyleCSS(bgCfg);
|
||
const motionCfg = motionConfigFromSettings(settings);
|
||
const motionHtmlClass = [
|
||
!motionCfg.ui_animations ? "j13-reduce-motion" : "",
|
||
motionCfg.ui_animations && !motionCfg.anim_chrome ? "j13-reduce-chrome" : "",
|
||
bgCfg.bg_site_url ? "j13-has-site-bg" : "",
|
||
bgCfg.bg_admin_url ? "j13-has-admin-bg" : "",
|
||
]
|
||
.filter(Boolean)
|
||
.join(" ");
|
||
|
||
let user: User | null = null;
|
||
let unread = 0;
|
||
let chatUnread = 0;
|
||
const hasAuthCookie = !!cookieStore.get(TOKEN_COOKIE)?.value;
|
||
if (hasAuthCookie) {
|
||
// 用户信息与未读数合并为一次请求;只转发最小化的 access cookie
|
||
const me = await getMeCached(authCookieHeader(cookieStore) || undefined);
|
||
user = me.user;
|
||
unread = me.unread_count;
|
||
chatUnread = me.chat_unread_count ?? 0;
|
||
}
|
||
|
||
return (
|
||
<html
|
||
lang="zh-CN"
|
||
data-j13-chrome={isAdmin ? "admin" : "site"}
|
||
className={`${theme === "dark" ? "dark" : ""} ${motionHtmlClass} ${jakarta.variable} ${notoSC.variable} ${jetbrainsMono.variable}`.trim()}
|
||
>
|
||
<body className="min-h-screen flex flex-col">
|
||
{accentCSS ? (
|
||
// React 托管的 SSR 主题节点;客户端换肤走 j13-accent-runtime,禁止对此外挂 remove
|
||
<style
|
||
id="j13-accent-vars"
|
||
data-accent-source="ssr"
|
||
dangerouslySetInnerHTML={{ __html: accentCSS }}
|
||
/>
|
||
) : null}
|
||
{bgCSS ? (
|
||
<style
|
||
id="j13-bg-vars"
|
||
data-bg-source="ssr"
|
||
dangerouslySetInnerHTML={{ __html: bgCSS }}
|
||
/>
|
||
) : null}
|
||
<a href="#main" className="skip-link">
|
||
跳到主要内容
|
||
</a>
|
||
<SiteBrandProvider initial={settings}>
|
||
<MotionProvider initial={motionCfg}>
|
||
<CommentsPolicyProvider allowComments={settings.allow_comments !== false}>
|
||
<MessagesPolicyProvider allowMessages={settings.allow_messages !== false}>
|
||
<PostLinkProvider openInNewTab={settings.post_link_new_tab !== false}>
|
||
<CodeFoldProvider
|
||
initialEnabled={settings.code_block_auto_fold}
|
||
initialFoldLines={settings.code_block_fold_lines}
|
||
>
|
||
<SiteChrome
|
||
initialIsAdmin={isAdmin}
|
||
user={user}
|
||
unread={unread}
|
||
chatUnread={settings.allow_messages !== false ? chatUnread : 0}
|
||
theme={theme}
|
||
allowRegister={settings.allow_register}
|
||
>
|
||
{children}
|
||
</SiteChrome>
|
||
</CodeFoldProvider>
|
||
</PostLinkProvider>
|
||
</MessagesPolicyProvider>
|
||
</CommentsPolicyProvider>
|
||
</MotionProvider>
|
||
</SiteBrandProvider>
|
||
<Toaster />
|
||
<TipHost />
|
||
<AccountGuard />
|
||
<ThemeSync initialAccent={accent || ""} initialBg={bgCfg} />
|
||
<VisitBeacon />
|
||
<RealtimeProvider userId={user ? user.id : null} />
|
||
</body>
|
||
</html>
|
||
);
|
||
}
|