layout.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import type { Metadata } from "next";
  2. import { Geist, Geist_Mono } from "next/font/google";
  3. import Script from "next/script";
  4. import React from "react";
  5. import "./globals.scss";
  6. import { SignalRProvider } from '@/contexts/signalrProvider';
  7. import { AuthProvider } from "@/contexts/authProvider";
  8. import { MemberProvider } from "@/contexts/memberProvider";
  9. import { ConfigProvider } from "@/contexts/configProvider";
  10. import { ThemeProvider } from "@/contexts/themeProvider";
  11. import { getAccessToken, getSignalRCryptoUrl, getSignalRChatUrl } from "@/lib/utils/server";
  12. import { fetchConfig } from "@/lib/api/system";
  13. const geistSans = Geist({
  14. variable: "--font-geist-sans",
  15. subsets: ["latin"],
  16. });
  17. const geistMono = Geist_Mono({
  18. variable: "--font-geist-mono",
  19. subsets: ["latin"],
  20. });
  21. type ColorSchemeEnum = 'normal' | 'light' | 'dark' | 'light dark' | 'dark light' | 'only light';
  22. function parseMetaAdds(html: string | null | undefined) {
  23. if (!html) return { other: {} as Record<string, string>, themeColor: [] as { media?: string; color: string }[], colorScheme: undefined as ColorSchemeEnum | undefined };
  24. const other: Record<string, string> = {};
  25. const themeColor: { media?: string; color: string }[] = [];
  26. const validColorSchemes: ColorSchemeEnum[] = ['normal', 'light', 'dark', 'light dark', 'dark light', 'only light'];
  27. let colorScheme: ColorSchemeEnum | undefined;
  28. const tagRegex = /<meta\s+([^>]+)\/?>/gi;
  29. const attrRegex = /(\w[\w-]*)=["']([^"']*?)["']/gi;
  30. let tagMatch;
  31. while ((tagMatch = tagRegex.exec(html)) !== null) {
  32. const attrs: Record<string, string> = {};
  33. let attrMatch;
  34. while ((attrMatch = attrRegex.exec(tagMatch[1])) !== null) {
  35. attrs[attrMatch[1].toLowerCase()] = attrMatch[2];
  36. }
  37. const name = attrs['name'];
  38. const content = attrs['content'];
  39. if (!name || !content) continue;
  40. if (name === 'theme-color') {
  41. themeColor.push(attrs['media'] ? { media: attrs['media'], color: content } : { color: content });
  42. } else if (name === 'color-scheme') {
  43. if (validColorSchemes.includes(content as ColorSchemeEnum)) {
  44. colorScheme = content as ColorSchemeEnum;
  45. }
  46. } else {
  47. other[name] = content;
  48. }
  49. }
  50. return { other, themeColor, colorScheme };
  51. }
  52. export async function generateMetadata(): Promise<Metadata> {
  53. const config = (await fetchConfig())?.data;
  54. const metaAdds = parseMetaAdds(config?.meta?.adds);
  55. return {
  56. // 정규 주소(canonical). './' 는 현재 경로로 해석되므로
  57. // 쿼리스트링이 붙은 주소가 별도 페이지로 색인되는 것을 막는다.
  58. metadataBase: new URL('https://bitforum.io'),
  59. alternates: { canonical: './' },
  60. title: config?.basic?.siteName ?? 'bitforum',
  61. description: config?.meta?.description ?? '',
  62. keywords: config?.meta?.keywords ?? '',
  63. authors: config?.meta?.author ? [{ name: config.meta.author }] : undefined,
  64. applicationName: config?.meta.applicationName,
  65. generator: config?.meta.generator,
  66. robots: config?.meta.robots,
  67. ...(metaAdds.themeColor.length > 0 && { themeColor: metaAdds.themeColor }),
  68. ...(metaAdds.colorScheme && { colorScheme: metaAdds.colorScheme }),
  69. other: {
  70. 'naver-site-verification': '18dc0d1c5cb466a765e5f5093f94638ed6537d1c',
  71. ...metaAdds.other,
  72. },
  73. };
  74. }
  75. export default async function RootLayout({
  76. children,
  77. }: Readonly<{
  78. children: React.ReactNode;
  79. }>) {
  80. const accessToken = await getAccessToken();
  81. const signalRCryptoUrl = await getSignalRCryptoUrl();
  82. const signalRChatUrl = await getSignalRChatUrl();
  83. const config = (await fetchConfig())?.data;
  84. return (
  85. <html lang="ko" suppressHydrationWarning>
  86. <head>
  87. <link rel="manifest" href="/manifest.json" />
  88. <Script src="https://www.googletagmanager.com/gtag/js?id=G-DY6YFW4CTM" strategy="afterInteractive" />
  89. <Script id="gtag-init" strategy="afterInteractive">
  90. {`
  91. window.dataLayer = window.dataLayer || [];
  92. function gtag(){dataLayer.push(arguments);}
  93. gtag('js', new Date());
  94. gtag('config', 'G-DY6YFW4CTM');
  95. `}
  96. </Script>
  97. </head>
  98. <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
  99. <ThemeProvider>
  100. <SignalRProvider accessToken={accessToken} signalRCryptoUrl={signalRCryptoUrl} signalRChatUrl={signalRChatUrl}>
  101. <AuthProvider>
  102. <MemberProvider>
  103. <ConfigProvider initialConfig={config}>
  104. {children}
  105. </ConfigProvider>
  106. </MemberProvider>
  107. </AuthProvider>
  108. </SignalRProvider>
  109. </ThemeProvider>
  110. </body>
  111. </html>
  112. );
  113. }