next/font/google laedt Outfit beim Bauen von Google — das macht den Build von einer Netzverbindung abhaengig und liess ihn im Container haengen. Die variable Fassung liegt jetzt als Datei im Repository; weder beim Bauen noch zur Laufzeit geht eine Anfrage nach aussen. Ausserdem: npm blockiert seit Version 11 Install-Skripte. Fuer prisma und @prisma/engines sind sie jetzt gezielt erlaubt, damit im Image die plattformpassende Engine bereitliegt.
95 lines
3.2 KiB
TypeScript
95 lines
3.2 KiB
TypeScript
import type { Metadata, Viewport } from "next";
|
||
import localFont from "next/font/local";
|
||
import { BrandDefs } from "@/components/brand/FlimmerLogo";
|
||
import { ToastProvider } from "@/components/Toasts";
|
||
import { ServiceWorkerRegistration } from "@/components/ServiceWorkerRegistration";
|
||
import { env } from "@/lib/env";
|
||
import "./globals.css";
|
||
|
||
/**
|
||
* Outfit liegt als Datei im Repository.
|
||
*
|
||
* next/font/google würde die Schrift beim Bauen herunterladen — das macht den
|
||
* Build von einer Netzverbindung abhängig und ließ ihn im Container hängen.
|
||
* Für eine selbst gehostete Anwendung ist die mitgelieferte Datei ohnehin das
|
||
* Richtige: kein Abruf beim Bauen, keiner zur Laufzeit.
|
||
*
|
||
* Es ist die variable Fassung (Strichstärke 100–900), aufgeteilt in Latin und
|
||
* Latin-Extended — der Browser lädt nur, was die Seite wirklich braucht.
|
||
*/
|
||
const outfit = localFont({
|
||
src: [
|
||
{ path: "../../public/fonts/outfit-latin.woff2", weight: "100 900", style: "normal" },
|
||
{ path: "../../public/fonts/outfit-latin-ext.woff2", weight: "100 900", style: "normal" },
|
||
],
|
||
display: "swap",
|
||
variable: "--font-outfit",
|
||
fallback: ["Inter", "Segoe UI", "system-ui", "sans-serif"],
|
||
});
|
||
|
||
export const metadata: Metadata = {
|
||
metadataBase: new URL(env.appUrl),
|
||
title: {
|
||
default: "Flimmer",
|
||
template: "%s · Flimmer",
|
||
},
|
||
description: "Serien und Filme tracken — selbst gehostet, ohne fremde Konten.",
|
||
applicationName: "Flimmer",
|
||
manifest: "/manifest.webmanifest",
|
||
icons: {
|
||
icon: [
|
||
{ url: "/favicon.svg", type: "image/svg+xml" },
|
||
{ url: "/favicon.ico", sizes: "48x48" },
|
||
],
|
||
apple: "/icons/apple-touch-icon.png",
|
||
},
|
||
openGraph: {
|
||
title: "Flimmer",
|
||
description: "Serien und Filme tracken — selbst gehostet, ohne fremde Konten.",
|
||
images: ["/og-image.png"],
|
||
type: "website",
|
||
},
|
||
};
|
||
|
||
export const viewport: Viewport = {
|
||
themeColor: [
|
||
{ media: "(prefers-color-scheme: dark)", color: "#0c0a14" },
|
||
{ media: "(prefers-color-scheme: light)", color: "#faf7f5" },
|
||
],
|
||
width: "device-width",
|
||
initialScale: 1,
|
||
viewportFit: "cover",
|
||
};
|
||
|
||
/**
|
||
* Setzt data-theme, bevor der erste Pixel gemalt wird — sonst blitzt beim
|
||
* Laden kurz das falsche Farbschema auf.
|
||
*/
|
||
const THEME_BOOTSTRAP = `(function(){try{
|
||
var stored=localStorage.getItem("flimmer-theme");
|
||
var mode=stored==="light"||stored==="dark"?stored:
|
||
(stored==="system"||!stored)&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";
|
||
document.documentElement.setAttribute("data-theme",mode);
|
||
}catch(e){document.documentElement.setAttribute("data-theme","dark");}})();`;
|
||
|
||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||
return (
|
||
<html lang="de" data-theme="dark" className={outfit.variable} suppressHydrationWarning>
|
||
<head>
|
||
<script dangerouslySetInnerHTML={{ __html: THEME_BOOTSTRAP }} />
|
||
</head>
|
||
<body className="antialiased">
|
||
<BrandDefs />
|
||
<a
|
||
href="#inhalt"
|
||
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 fl-btn fl-btn-primary"
|
||
>
|
||
Zum Inhalt springen
|
||
</a>
|
||
<ToastProvider>{children}</ToastProvider>
|
||
<ServiceWorkerRegistration />
|
||
</body>
|
||
</html>
|
||
);
|
||
}
|