Files
jiang13-bbs/frontend/app/layout.tsx
freefire 1bb565c687 feat(frontend+backend): 实现SSR登录态优化与字体自托管
1. 后端调整refresh cookie路径与注释,完善认证cookie处理逻辑
2. 前端全局layout新增next/font自托管字体,消除FOUT闪动
3. 实现SSR直出登录态与未读通知数,避免页面加载闪动
4. 新增中间件处理access token静默轮转,保障SSR登录态
5. 优化登录、改密流程,添加页面刷新同步登录态
6. 重构通知铃铛组件支持初始未读数传入
2026-09-12 04:59:26 +08:00

95 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { fetchMe, fetchUnreadCount, type User } from "@/lib/api";
// 构建期下载并自托管字体:同源加载 + 自动 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 中的右上角就是正确的用户区,而非先闪"登录/注册"
const cookieHeader = cookieStore.toString();
let user: User | null = null;
let unread = 0;
if (cookieStore.get("j13_refresh")?.value) {
try {
const [me, count] = await Promise.all([
fetchMe(cookieHeader),
fetchUnreadCount(cookieHeader),
]);
user = me.user;
unread = count;
} catch {
// 后端不可用时降级为游客视图,不阻塞页面渲染
}
}
return (
<html
lang="zh-CN"
className={`${theme === "dark" ? "dark" : ""} ${jakarta.variable} ${notoSC.variable} ${jetbrainsMono.variable}`}
>
<body className="min-h-screen flex flex-col">
<a href="#main" className="skip-link">
跳到主要内容
</a>
<Header initialUser={user} initialUnread={unread} initialTheme={theme} />
<main
id="main"
tabIndex={-1}
className="flex-1 w-full max-w-6xl mx-auto px-4 sm:px-6 py-7 sm:py-9 outline-none"
>
{children}
</main>
<Footer />
<Toaster />
</body>
</html>
);
}