import type { Metadata } from "next"; import { cookies } from "next/headers"; import { Plus_Jakarta_Sans, Noto_Sans_SC, JetBrains_Mono } from "next/font/google"; import "./globals.css"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; import Toaster from "@/components/Toaster"; import type { User } from "@/lib/api"; import { getMeCached, getPublicSettingsCached } from "@/lib/serverData"; import { TOKEN_COOKIE, REFRESH_COOKIE, authCookieHeader } from "@/lib/cookies"; import { accentStyleCSS } from "@/lib/theme"; // 构建期下载并自托管字体:同源加载 + 自动 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({ weight: ["400", "500", "700", "900"], variable: "--font-noto-sc", display: "swap", }); const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], weight: ["400", "500", "700"], variable: "--font-jetbrains", display: "swap", }); export const metadata: Metadata = { title: { default: "姜十三论坛 · J13 FORUM", template: "%s · 姜十三论坛", }, description: "姜十三论坛 - 技术分享与讨论社区", openGraph: { title: "姜十三论坛 · J13 FORUM", description: "姜十三论坛 - 技术分享与讨论社区", type: "website", }, }; export default async function RootLayout({ children, }: { children: React.ReactNode; }) { const cookieStore = await cookies(); // 浅色优先的设计系统,仅在显式选择暗色时挂 .dark const theme = cookieStore.get("j13-theme")?.value === "dark" ? "dark" : "light"; // SSR 直出登录态(access 过期时 middleware 已先行轮转,见 middleware.ts), // 保证 F5 初始 HTML 中的右上角就是正确的用户区,而非先闪"登录/注册"。 // 旧版 refresh cookie 可能是 Path=/api/auth(页面请求不带),因此只要 // token 或 refresh 任一存在就向后端确认一次,避免半登录态(列表点赞态与 // Header 游客态不一致),并兼容 access 15 分钟内的自然窗口。 // 站点主题色(公开设置):派生浅/暗两套 accent CSS 变量后随首屏 HTML 内联, // 覆盖 globals.css 默认令牌——无主题闪动(FOUC),后端不可用时降级为默认靛蓝 const { accent } = await getPublicSettingsCached(); const accentCSS = accentStyleCSS(accent || ""); let user: User | null = null; let unread = 0; const hasAuthCookie = !!cookieStore.get(TOKEN_COOKIE)?.value || !!cookieStore.get(REFRESH_COOKIE)?.value; if (hasAuthCookie) { // 用户信息与未读数合并为一次请求;只转发最小化的 access cookie const me = await getMeCached(authCookieHeader(cookieStore) || undefined); user = me.user; unread = me.unread_count; } return (
{accentCSS ? ( // id 与 lib/theme 的运行时换肤共用:后台实时预览时直接复用/替换此节点 ) : null} 跳到主要内容