mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
NEXT_PUBLIC_BUNNY_CDN_URL is inlined into the client bundle at build time, and the published image is built by CI without it, so the browser had no host to build a playlist URL from no matter what the operator set in .env.docker. The player read the empty URL as a stream that had not finished encoding and sat on 'Video Is Processing', retrying forever. The server knows the value on every request, so the root layout now serialises the public settings into a JSON script tag and the browser reads them from there, falling back to the build-time variable for source builds. The direct-download allow list came through the same broken path and moves with it. Closes #60
167 lines
4.5 KiB
TypeScript
167 lines
4.5 KiB
TypeScript
import type { Metadata } from 'next';
|
|
import { Geist_Mono, JetBrains_Mono } from 'next/font/google';
|
|
import { Toaster } from 'sonner';
|
|
import { ThemeProvider } from '@/components/theme-provider';
|
|
import {
|
|
buildRuntimePublicConfig,
|
|
RUNTIME_PUBLIC_CONFIG_ELEMENT_ID,
|
|
} from '@/lib/runtime-public-config';
|
|
import { seoConfig } from '@/lib/seo';
|
|
import './globals.css';
|
|
|
|
const jetbrainsMono = JetBrains_Mono({
|
|
subsets: ['latin'],
|
|
variable: '--font-sans',
|
|
display: 'swap',
|
|
preload: true,
|
|
});
|
|
|
|
const geistMono = Geist_Mono({
|
|
subsets: ['latin'],
|
|
variable: '--font-geist-mono',
|
|
display: 'swap',
|
|
preload: true,
|
|
});
|
|
|
|
export const metadata: Metadata = {
|
|
metadataBase: new URL(seoConfig.url),
|
|
title: {
|
|
default: `${seoConfig.name} | ${seoConfig.title}`,
|
|
template: `%s | ${seoConfig.name}`,
|
|
},
|
|
description: seoConfig.description,
|
|
applicationName: seoConfig.name,
|
|
keywords: [...seoConfig.keywords],
|
|
authors: [{ name: seoConfig.name, url: seoConfig.url }],
|
|
creator: seoConfig.name,
|
|
publisher: seoConfig.name,
|
|
category: 'technology',
|
|
referrer: 'no-referrer',
|
|
alternates: {
|
|
canonical: '/',
|
|
},
|
|
icons: {
|
|
icon: [{ url: seoConfig.logo, type: 'image/svg+xml' }],
|
|
shortcut: [seoConfig.logo],
|
|
apple: [{ url: seoConfig.logo }],
|
|
},
|
|
manifest: '/manifest.webmanifest',
|
|
openGraph: {
|
|
type: 'website',
|
|
locale: 'en_US',
|
|
siteName: seoConfig.name,
|
|
url: seoConfig.url,
|
|
title: `${seoConfig.name} | ${seoConfig.title}`,
|
|
description: seoConfig.description,
|
|
images: [
|
|
{
|
|
url: seoConfig.ogImage,
|
|
width: 1888,
|
|
height: 1048,
|
|
alt: `${seoConfig.name} meta image`,
|
|
},
|
|
],
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title: `${seoConfig.name} | ${seoConfig.title}`,
|
|
description: seoConfig.description,
|
|
images: [seoConfig.ogImage],
|
|
},
|
|
robots: {
|
|
index: true,
|
|
follow: true,
|
|
googleBot: {
|
|
index: true,
|
|
follow: true,
|
|
'max-image-preview': 'large',
|
|
'max-snippet': -1,
|
|
'max-video-preview': -1,
|
|
},
|
|
},
|
|
formatDetection: {
|
|
email: false,
|
|
address: false,
|
|
telephone: false,
|
|
},
|
|
};
|
|
|
|
const structuredData = [
|
|
{
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Organization',
|
|
name: seoConfig.name,
|
|
url: seoConfig.url,
|
|
logo: `${seoConfig.url}${seoConfig.logoPath}`,
|
|
sameAs: [seoConfig.githubUrl],
|
|
},
|
|
{
|
|
'@context': 'https://schema.org',
|
|
'@type': 'WebSite',
|
|
name: seoConfig.name,
|
|
url: seoConfig.url,
|
|
description: seoConfig.description,
|
|
publisher: {
|
|
'@type': 'Organization',
|
|
name: seoConfig.name,
|
|
logo: {
|
|
'@type': 'ImageObject',
|
|
url: `${seoConfig.url}${seoConfig.logoPath}`,
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
export default function RootLayout({
|
|
children,
|
|
}: Readonly<{
|
|
children: React.ReactNode;
|
|
}>) {
|
|
return (
|
|
<html
|
|
lang="en"
|
|
className={`${jetbrainsMono.variable} ${geistMono.variable}`}
|
|
suppressHydrationWarning
|
|
>
|
|
<body className="antialiased min-h-screen bg-background font-sans">
|
|
{/* Not executed, only parsed by readRuntimePublicConfig(). It carries the
|
|
public settings the browser cannot get from NEXT_PUBLIC_* variables,
|
|
which are frozen into the bundle when the image is built. */}
|
|
<script
|
|
id={RUNTIME_PUBLIC_CONFIG_ELEMENT_ID}
|
|
type="application/json"
|
|
dangerouslySetInnerHTML={{
|
|
__html: JSON.stringify(buildRuntimePublicConfig()).replace(/</g, '\\u003c'),
|
|
}}
|
|
/>
|
|
{/* One script per object: single-object payloads with a top-level
|
|
@context survive naive JSON-LD consumers that choke on arrays. */}
|
|
{structuredData.map((data) => (
|
|
<script
|
|
key={String(data['@type'])}
|
|
type="application/ld+json"
|
|
dangerouslySetInnerHTML={{
|
|
__html: JSON.stringify(data).replace(/</g, '\\u003c'),
|
|
}}
|
|
/>
|
|
))}
|
|
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
|
|
<svg aria-hidden="true" className="fixed h-0 w-0">
|
|
<filter id="openframe-noise">
|
|
<feTurbulence
|
|
type="fractalNoise"
|
|
baseFrequency="0.92"
|
|
numOctaves="2"
|
|
stitchTiles="stitch"
|
|
/>
|
|
</filter>
|
|
</svg>
|
|
{children}
|
|
<div aria-hidden="true" className="noise-overlay" />
|
|
<Toaster />
|
|
</ThemeProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|