import OperationalBanner from "@/components/OperationalBanner"; import type { Metadata, Viewport } 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 { levelVarsCSS } from "@/lib/levels"; import { siteDocumentTitle } from "@/lib/brand"; import { motionConfigFromSettings } from "@/lib/motion"; import { peekPublicMetaSnapshot, publishPublicMetaSnapshot } from "@/lib/publicMetaSnapshot"; import { metadataBaseURL } from "@/lib/siteUrl"; // 构建期下载并自托管字体:同源加载 + 自动 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", }); // 必须同步:根 layout 若在此 await(拉设置 / headers),Next 流式 metadata // 会在等待期把标签临时写成 localhost:3000,开发态 RSC/HMR 一密就来回闪。 export function generateMetadata(): Metadata { const settings = peekPublicMetaSnapshot(); 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; const metadataBase = metadataBaseURL(); return { ...(metadataBase ? { metadataBase } : {}), title: { default: title, template: `%s · ${name}`, }, description: desc, keywords, // 固定地址:换图后浏览器/书签侧的 icon 链接不变 ...(settings.favicon_url ? { icons: { icon: "/api/brand/favicon" } } : {}), openGraph: { title, description: desc, type: "website", }, }; } // 移动端键盘弹出时压缩布局高度(而非仅视觉平移),聊天输入框随 100dvh // 收缩保持可见,配合聊天页锁视口布局;再叠加聚焦时 scrollIntoView 兜底 export const viewport: Viewport = { width: "device-width", initialScale: 1, interactiveWidget: "resizes-content", }; 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(); publishPublicMetaSnapshot(settings); 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 (
{accentCSS ? ( // React 托管的 SSR 主题节点;客户端换肤走 j13-accent-runtime,禁止对此外挂 remove ) : null} {bgCSS ? ( ) : null} {/* 等级配色变量(--lv-N-fg/-bg,引用设计 token 随明暗主题切换);恒渲染兼作兜底 */} 跳到主要内容