openreader/src/app/layout.tsx
Richard R 27267e5ffb refactor(public): use environment variables for base URL and improve accessibility
Switch landing and layout metadata to use BASE_URL from environment for
deployment flexibility. Refine privacy page environment check for robustness.
Update ARIA attributes on landing preview for better accessibility. Adjust
terminal and visually hidden styles for improved formatting and screen reader
support.
2026-05-31 16:29:40 -06:00

79 lines
2.7 KiB
TypeScript

import "./globals.css";
import { ReactNode } from "react";
import { Metadata, Viewport } from "next";
import { Figtree } from "next/font/google";
import { ConsentAwareAnalytics } from "@/components/ConsentAwareAnalytics";
import { CookieConsentBanner } from "@/components/CookieConsentBanner";
import { getResolvedRuntimeConfig } from "@/lib/server/runtime-config";
import pkg from "../../package.json";
const figtree = Figtree({
subsets: ["latin"],
weight: ["300", "400", "500", "600", "700", "800"],
display: "swap",
variable: "--font-display",
});
const themeInitScript = `
(() => {
const themes = ['system', 'light', 'dark', 'ocean', 'forest', 'sunset', 'sea', 'mint', 'lavender', 'rose', 'sand', 'sky', 'slate'];
const lightThemes = new Set(['light', 'lavender', 'rose', 'sand', 'sky', 'slate']);
const root = document.documentElement;
const stored = localStorage.getItem('theme');
const selected = stored && themes.includes(stored) ? stored : 'system';
const effective = selected === 'system'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: selected;
root.classList.remove(...themes);
root.classList.add(effective);
root.style.colorScheme = lightThemes.has(effective) ? 'light' : 'dark';
})();
`;
export const viewport: Viewport = {
viewportFit: "cover",
};
export const metadata: Metadata = {
title: {
default: "OpenReader",
template: "%s | OpenReader",
},
manifest: "/manifest.json",
metadataBase: new URL(process.env.BASE_URL || "https://openreader.richardr.dev"),
verification: {
google: "MJXyTudn1kgQF8EtGD-tsnAWev7Iawso9hEvqeGHB3U",
},
};
function jsonEmbedSafe(value: unknown): string {
// Prevent `</script>` and U+2028/U+2029 from breaking the inline script.
return JSON.stringify(value)
.replace(/</g, '\\u003c')
.replace(/\u2028/g, "\\u2028")
.replace(/\u2029/g, "\\u2029");
}
export default async function RootLayout({ children }: { children: ReactNode }) {
const runtimeConfig = await getResolvedRuntimeConfig();
const runtimeConfigWithAppVersion = {
...runtimeConfig,
appVersion: pkg.version,
};
const runtimeConfigInit = `window.__RUNTIME_CONFIG__=${jsonEmbedSafe(runtimeConfigWithAppVersion)};`;
return (
<html lang="en" className={figtree.variable} suppressHydrationWarning>
<head>
<meta name="color-scheme" content="light dark" />
<script dangerouslySetInnerHTML={{ __html: runtimeConfigInit }} />
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
</head>
<body className="antialiased">
{children}
<CookieConsentBanner />
<ConsentAwareAnalytics />
</body>
</html>
);
}