Files
EpicNext-Cms/src/app/layout.tsx
T
openhands 990ebdb158
Gitea Actions Runner Test / test-job (push) Successful in 2s
CI / check (push) Successful in 35s
CI / tests-integration (push) Successful in 1m58s
CI / tests-unit (push) Successful in 2m22s
CI / tests-ui (push) Successful in 2m50s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m50s
fix(pwa): retire the service worker instead of just fixing it
The previous commit stopped the worker from caching build output, which
removed the cause of the blank Catalog Studio but left the worker itself
serving a purpose worth one offline fallback: three public endpoints
(/api/home, /api/online, /api/radio/config). Behind Cloudflare, for a site
whose visitors are online, that fallback rarely fires and goes stale exactly
where it is most likely to matter.

So the worker goes away rather than staying as a mostly-inert layer that every
future release still has to keep correct.

public/sw.js is not deleted, because deleting it would leave every existing
registration alive and still in control of the page, caching as it did before.
It becomes the opposite of what it was: on activate it deletes every cache,
unregisters itself and reloads open clients so they stop being controlled.
Browsers that already installed a worker therefore uninstall it on their next
visit; browsers that never had one are unaffected.

src/components/pwa-register.tsx is removed and unmounted from the root
layout, so nothing registers a worker any more and the kill switch only ever
runs for the visitors who need it.

The manifest is deliberately untouched. src/app/manifest.ts is an ordinary
Next.js route, independent of any worker, and Chrome installs from a manifest
alone, so the site stays installable on a phone.

Verified nothing depended on it: no other navigator.serviceWorker or caches.*
reference exists in the app, the theme runs from the plain /scripts/
theme-init.js script, and no Studio, catalog or import module touches a
worker. Typecheck and lint clean, 2326 unit tests and 72 UI tests green,
including every Studio and catalog spec.
2026-10-11 17:31:15 +02:00

117 lines
3.9 KiB
TypeScript

import type { Metadata } from "next";
import { Nunito, Pixelify_Sans } from "next/font/google";
import { headers } from "next/headers";
import Script from "next/script";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages } from "next-intl/server";
import { type ReactNode, Suspense } from "react";
import { GlobalProgressBar } from "@/components/global-progress-bar";
import { ScopedThemeVars } from "@/components/scoped-theme-vars";
import { ThemeScopeDetector } from "@/components/theme-scope-detector";
import { ThemeVars } from "@/components/theme-vars";
import { enforceSiteAccess } from "@/lib/access-guard";
import { getDefaultFaviconDataUri } from "@/lib/default-favicon";
import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
import "./globals.css";
const nunito = Nunito({
subsets: ["latin"],
weight: ["400", "600", "700", "800"],
variable: "--font-nunito",
display: "swap",
});
const pixelify = Pixelify_Sans({
subsets: ["latin"],
weight: ["400", "700"],
variable: "--font-pixel",
display: "swap",
});
// The root layout reads runtime data (locale cookie, request headers via
// enforceSiteAccess, DB-backed site settings) to render the <html> element,
// which cannot be wrapped in <Suspense>. Child pages inherit this opt-out,
// so leaf pages that only access cached/DB data stay instant.
// export const instant = false;
export async function generateMetadata(): Promise<Metadata> {
const appUrl = process.env.APP_URL ?? "http://localhost:3000";
const hotel = await resolveHotelName();
const customFavicon = await siteSettings.get("cms_favicon");
const faviconUrl = customFavicon ?? getDefaultFaviconDataUri();
const icons = [
{ rel: "icon", url: faviconUrl },
{ rel: "shortcut icon", url: faviconUrl },
];
return {
title: { default: `${hotel} - Hotel`, template: `%s · ${hotel}` },
description: `${hotel} — a Habbo retro hotel. Hang out, collect furni and meet friends.`,
metadataBase: new URL(appUrl),
alternates: { canonical: appUrl },
icons,
openGraph: {
title: `${hotel} - Hotel`,
description: `${hotel} — a Habbo retro hotel. Hang out, collect furni and meet friends.`,
type: "website",
siteName: hotel,
},
twitter: { card: "summary_large_image", title: `${hotel} - Hotel` },
};
}
export default async function RootLayout({
children,
}: {
children: ReactNode;
}) {
await enforceSiteAccess();
const locale = await getLocale();
const messages = await getMessages();
const defaultDark = await siteSettings.getBool("default_dark", false);
const nitroUrl = await siteSettings.get("nitro_client_url", "");
const nonce = (await headers()).get("x-nonce") ?? undefined;
return (
<html
lang={locale}
className={`app ${nunito.variable} ${pixelify.variable}`}
suppressHydrationWarning
>
<head>
<meta name="theme-default-dark" content={String(defaultDark)} />
{/* Scroll-revealed content starts transparent and is only shown by a
client-side animation. Without JavaScript that animation never
runs, so force it visible instead of leaving the page blank. */}
<noscript>
<style>{".motion-reveal{opacity:1 !important}"}</style>
</noscript>
<Script
src="/scripts/theme-init.js"
strategy="beforeInteractive"
nonce={nonce}
/>
<link rel="preconnect" href="https://www.habbo.com" />
{nitroUrl?.startsWith("http") ? (
<link rel="preconnect" href={new URL(nitroUrl).origin} />
) : null}
</head>
<body
className="flex min-h-screen flex-col site-bg"
style={{ backgroundColor: "var(--color-background)" }}
>
<Suspense fallback={null}>
<GlobalProgressBar />
</Suspense>
<NextIntlClientProvider locale={locale} messages={messages}>
<ThemeVars nonce={nonce} />
<ScopedThemeVars nonce={nonce} />
<ThemeScopeDetector />
{children}
</NextIntlClientProvider>
</body>
</html>
);
}