Files
novalon-website/src/app/layout.tsx
T
zhangxiang f8917d4fda feat(ui): 营销站 UI/UE/UX 治理 P0→P3 收官
复评(impeccable critique)全部 Priority Issues 与 Minor Observations 落地:
- P0 数字口径:MetricsBasisNote 单一真源 + metrics-basis.test fs 防线,接入
  products/solutions/services/home/about;纯渲染端加角注,不动 DB/seed 值
- P1 转化断头路:敬请期待态 + 404 语境出口 + services 空态双出口
- P1 首页并卡:TrustSection 早鸟横幅并入 CasesSection 单张深色共创卡
- P2 CTA 治理:CONSULT_CTA_ALLOWLIST 白名单 + 逐字符扫描器,/contact CTA 全归一
- P3 polish:footer 补 服务/公司列、contact 死三元/空槽/toast、GlobalErrorTracker 幂等日志
- 文档同源:PRODUCT/CONTEXT 动效带统一 180–280ms;DESIGN.md 立 The 10px Floor;
  font-floor.test 守卫禁止再引入 <10px(保留 10/11px 有意 Bain 微标签)

Gates: type-check 0 · jest 1581/132 · lint 0e/126w · contrast 7/7 · headings 10/10
2026-09-20 11:50:58 +08:00

229 lines
9.9 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, Viewport } from "next";
import localFont from "next/font/local";
import "./globals.css";
import { Suspense } from "react";
import { GoogleAnalyticsWrapper } from "@/components/analytics/GoogleAnalyticsWrapper";
import { GlobalErrorTracker } from "@/components/analytics/GlobalErrorTracker";
import { CookieConsent } from "@/components/analytics/CookieConsent";
import { PerformanceTracker } from "@/components/analytics/PerformanceTracker";
import { OutboundLinkTracker } from "@/components/analytics/OutboundLinkTracker";
import { ScrollDepthTracker } from "@/components/analytics/ScrollDepthTracker";
import { OrganizationSchema, WebsiteSchema, LocalBusinessSchema } from "@/components/seo/structured-data";
import { MobileTabBar } from "@/components/layout/mobile-tab-bar";
import { ErrorBoundary } from "@/components/ui/error-boundary";
import { ScrollProgress } from "@/components/ui/scroll-progress";
import { BackToTop } from "@/components/ui/back-to-top";
import { ClientLayout } from "@/components/layout/client-layout";
import { getPublishedItems } from "@/lib/cms/data-server";
import { SiteConfigProvider, type SiteConfig, type NavigationItem, type MegaDropdownGroup, type MegaDropdownItem } from "@/lib/site-config";
import { COMPANY_INFO, BRAND_NARRATIVE, type BrandPillar } from "@/lib/constants/company";
import { NAVIGATION_V2, MEGA_DROPDOWN_DATA } from "@/lib/constants/navigation";
/**
* 拉丁字形使用 Geist(本地 woff2,无网络请求);
* 中文由 globals.css 的 --font-sans 回退到 PingFang SC / 微软雅黑。
* 这样英文数字有设计字体,中文又不需下载数 MB 的 CJK 字重。
* 两者总重 140KBdisplay: swap 不阻塞首屏。
*/
const geistSans = localFont({
src: "./fonts/geist-sans.woff2",
variable: "--font-geist-sans",
display: "swap",
preload: true,
});
const geistMono = localFont({
src: "./fonts/geist-mono.woff2",
variable: "--font-geist-mono",
display: "swap",
preload: false,
});
export const metadata: Metadata = {
metadataBase: new URL("https://www.novalon.cn"),
title: {
default: "四川睿新致远科技有限公司 - 企业数字化转型服务商",
template: "%s | 四川睿新致远科技有限公司",
},
description: "四川睿新致远科技有限公司成立于2026年1月,专注于企业数字化转型服务,提供软件开发、云计算、数据分析、信息安全等一站式解决方案。",
keywords: ["数字化转型", "企业软件", "ERP系统", "CRM系统", "云计算", "数据分析", "软件开发", "成都科技公司", "金融科技", "睿新致远"],
authors: [{ name: "四川睿新致远科技有限公司" }],
creator: "四川睿新致远科技有限公司",
publisher: "四川睿新致远科技有限公司",
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
openGraph: {
type: "website",
locale: "zh_CN",
url: "https://www.novalon.cn",
siteName: "四川睿新致远科技有限公司",
title: "四川睿新致远科技有限公司 - 企业数字化转型服务商",
description: "专注于企业数字化转型服务,提供软件开发、云计算、数据分析、信息安全等一站式解决方案",
images: [
{
url: "/og-image.jpg",
width: 1200,
height: 630,
alt: "四川睿新致远科技有限公司",
},
],
},
twitter: {
card: "summary_large_image",
title: "四川睿新致远科技有限公司 - 企业数字化转型服务商",
description: "专注于企业数字化转型服务,提供软件开发、云计算、数据分析、信息安全等一站式解决方案",
images: ["/og-image.jpg"],
},
alternates: {
canonical: "https://www.novalon.cn",
},
verification: {
google: process.env.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION || "",
},
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 5,
themeColor: "#FFFFFF",
};
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
// Fetch site configuration, navigation, and standalone products from CMS
const [navItems, configItems, standaloneItems] = await Promise.all([
getPublishedItems('navigation').catch(() => []),
getPublishedItems('site-config').catch(() => []),
getPublishedItems('standalone-product').catch(() => []),
]);
const navData = navItems[0]?.data as Record<string, unknown> | undefined;
const configData = configItems[0]?.data as Record<string, unknown> | undefined;
// Generate standalone product navigation items from CMS data
const standaloneNavItems: MegaDropdownItem[] = standaloneItems.map((item) => {
const data = item.data as Record<string, unknown>;
const externalUrl = data.externalUrl as string | undefined;
return {
id: (data.id as string) || item.slug || '',
title: (data.title as string) || item.title,
description: (data.description as string) || '',
href: externalUrl || `/products/standalone/${item.slug || data.id}`,
badge: (data.status as string) === '内测中' ? '内测中' : undefined,
};
});
// Build megaDropdown with standalone products dynamically injected
const baseMegaDropdown = (navData?.megaDropdown as Record<string, MegaDropdownGroup[]>) || MEGA_DROPDOWN_DATA;
const megaDropdown: Record<string, MegaDropdownGroup[]> = {};
for (const [key, groups] of Object.entries(baseMegaDropdown)) {
if (key === 'products' && standaloneNavItems.length > 0) {
megaDropdown[key] = groups.map((group) => {
if (group.id === 'standalone-products') {
return { ...group, description: undefined, items: standaloneNavItems };
}
return group;
});
} else {
megaDropdown[key] = groups;
}
}
const siteConfig: SiteConfig = {
name: (configData?.name as string) || COMPANY_INFO.name,
shortName: (configData?.shortName as string) || COMPANY_INFO.shortName,
displayName: (configData?.displayName as string) || COMPANY_INFO.displayName,
slogan: (configData?.slogan as string) || COMPANY_INFO.slogan,
description: (configData?.description as string) || COMPANY_INFO.description,
valueProposition: (configData?.valueProposition as string) || BRAND_NARRATIVE.valueProposition,
positioningStatement: (configData?.positioningStatement as string) || BRAND_NARRATIVE.positioningStatement,
brandPromise: (configData?.brandPromise as string) || BRAND_NARRATIVE.brandPromise,
toneOfVoice: (configData?.toneOfVoice as string) || BRAND_NARRATIVE.toneOfVoice,
pillars: (Array.isArray(configData?.pillars) ? configData.pillars : BRAND_NARRATIVE.pillars) as BrandPillar[],
founded: (configData?.founded as string) || COMPANY_INFO.founded,
location: (configData?.location as string) || COMPANY_INFO.location,
email: (configData?.email as string) || COMPANY_INFO.email,
address: (configData?.address as string) || COMPANY_INFO.address,
icp: (configData?.icp as string) || COMPANY_INFO.icp,
police: (configData?.police as string) || COMPANY_INFO.police,
mainNav: (navData?.mainNav as NavigationItem[]) || NAVIGATION_V2,
megaDropdown,
};
return (
<html
lang="zh-CN"
className={`scroll-smooth ${geistSans.variable} ${geistMono.variable}`}
data-scroll-behavior="smooth"
suppressHydrationWarning
>
<head>
{/* 主题防 FOUC:首帧前同步解析偏好并写入 data-theme,避免刷新闪白/闪黑。
三档语义(与 src/lib/theme.ts 严格一致,改动时两处必须同步):
'light' | 'dark' → 用户显式选择,直接生效
'system' | 无值 → 跟随操作系统 prefers-color-scheme
显式选择优先于系统偏好;localStorage 不可用(隐私模式)时兜底浅色。 */}
<script
dangerouslySetInnerHTML={{
__html: `(function(){try{var p=localStorage.getItem('novalon-theme');if(p!=='light'&&p!=='dark'&&p!=='system'){p='system';}var t=p;if(p==='system'){t=(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light';}document.documentElement.setAttribute('data-theme',t);}catch(e){try{document.documentElement.setAttribute('data-theme','light');}catch(e2){}}})();`,
}}
/>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#C41E3A" />
<link rel="dns-prefetch" href="//formsubmit.co" />
<link rel="preconnect" href="//formsubmit.co" crossOrigin="anonymous" />
<OrganizationSchema />
<WebsiteSchema />
<LocalBusinessSchema />
</head>
<body
className="font-sans antialiased"
>
<a
href="#main-content"
data-skip-to-content="true"
className="absolute left-[-9999px] top-0 z-50 px-4 py-2 bg-brand text-white rounded-br-lg shadow-lg focus:left-0 focus:outline-none"
>
跳转到主要内容
</a>
<ScrollProgress />
<GoogleAnalyticsWrapper />
<GlobalErrorTracker />
<PerformanceTracker />
<OutboundLinkTracker />
<ScrollDepthTracker />
<ErrorBoundary>
<SiteConfigProvider config={siteConfig}>
<ClientLayout>
{children}
</ClientLayout>
</SiteConfigProvider>
</ErrorBoundary>
<Suspense fallback={null}>
<MobileTabBar />
</Suspense>
<CookieConsent />
<BackToTop />
</body>
</html>
);
}