diff --git a/src/actions/email-verify.ts b/src/actions/email-verify.ts
index 9cb351bb..456385a0 100644
--- a/src/actions/email-verify.ts
+++ b/src/actions/email-verify.ts
@@ -3,8 +3,8 @@
import { createHmac, timingSafeEqual } from "node:crypto";
import { getTranslations } from "next-intl/server";
import { env } from "@/env";
+import { resolveHotelName } from "@/lib/hotel-name";
import { sendMail } from "@/lib/services/email";
-import { siteSettings } from "@/lib/services/site-settings";
// Stateless email verification with a time-limited HMAC token.
//
@@ -78,8 +78,7 @@ export async function sendVerification(email: string): Promise {
const base = env.APP_URL.replace(/\/+$/, "");
const link = `${base}/verify?token=${encodeURIComponent(token)}&email=${encodeURIComponent(normalised)}`;
- const hotelName =
- (await siteSettings.get("hotel_name", env.HOTEL_NAME)) ?? env.HOTEL_NAME;
+ const hotelName = await resolveHotelName();
let subject = `Verify your email · ${hotelName}`;
let heading = "Verify your email";
diff --git a/src/app/(site)/apply/staff/page.tsx b/src/app/(site)/apply/staff/page.tsx
index bf16a818..6ebc1649 100644
--- a/src/app/(site)/apply/staff/page.tsx
+++ b/src/app/(site)/apply/staff/page.tsx
@@ -5,7 +5,7 @@ import { applyStaff } from "@/actions/applications";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
-import { siteSettings } from "@/lib/services/site-settings";
+import { resolveHotelName } from "@/lib/hotel-name";
export const dynamic = "force-dynamic";
@@ -53,7 +53,7 @@ export default async function ApplyStaffPage({
: null;
const userId = Number(session.user.id);
- const hotelName = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
+ const hotelName = await resolveHotelName();
function formatDeadline(d: Date | null | undefined): string {
if (!d) return t("noDeadline");
diff --git a/src/app/(site)/apply/team/page.tsx b/src/app/(site)/apply/team/page.tsx
index 790cfa2f..907b9b36 100644
--- a/src/app/(site)/apply/team/page.tsx
+++ b/src/app/(site)/apply/team/page.tsx
@@ -5,7 +5,7 @@ import { applyTeam } from "@/actions/applications";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
-import { siteSettings } from "@/lib/services/site-settings";
+import { resolveHotelName } from "@/lib/hotel-name";
export const dynamic = "force-dynamic";
@@ -53,7 +53,7 @@ export default async function ApplyTeamPage({
: null;
const userId = Number(session.user.id);
- const hotelName = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
+ const hotelName = await resolveHotelName();
// ── Teams open for application ──────────────────────────────
// The Prisma slice exposes teams directly (no separate team-position table
diff --git a/src/app/(site)/help/[category]/page.tsx b/src/app/(site)/help/[category]/page.tsx
index 8f3128f9..6e7c1a7a 100644
--- a/src/app/(site)/help/[category]/page.tsx
+++ b/src/app/(site)/help/[category]/page.tsx
@@ -5,7 +5,7 @@ import { ContentCard } from "@/components/public/ui";
import { prisma } from "@/lib/prisma";
import { sanitize } from "@/lib/sanitize";
import { SanitizedHtml } from "@/components/shared/sanitized-html";
-import { siteSettings } from "@/lib/services/site-settings";
+import { resolveHotelName } from "@/lib/hotel-name";
export const revalidate = 300;
@@ -75,7 +75,7 @@ export default async function HelpCategoryPage({
if (!cat) notFound();
- const hotelName = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
+ const hotelName = await resolveHotelName();
// Sibling help topics, so the reader can jump between categories. Isolated
// query: a DB hiccup degrades this list to empty rather than 500-ing.
diff --git a/src/app/(site)/login/page.tsx b/src/app/(site)/login/page.tsx
index ce7f041b..cd7c44e7 100644
--- a/src/app/(site)/login/page.tsx
+++ b/src/app/(site)/login/page.tsx
@@ -7,13 +7,14 @@ import { cached } from "@/lib/cache";
import { avatarImageUrl } from "@/lib/format";
import { prisma } from "@/lib/prisma";
import { captchaConfig } from "@/lib/services/captcha";
+import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
export default async function LoginPage() {
const t = await getTranslations("pages.login");
const th = await getTranslations("header");
const [hotelName, imagerRaw, cfg] = await Promise.all([
- siteSettings.get("hotel_name", "Atom"),
+ resolveHotelName(),
siteSettings.get(
"habbo_imaging_url",
"https://www.habbo.com/habbo-imaging/avatarimage",
@@ -87,7 +88,7 @@ export default async function LoginPage() {
className="text-xs mt-1 mx-auto max-w-[200px] leading-relaxed"
style={{ color: "var(--color-text-muted)" }}
>
- {t("welcomeBackSub", { hotelName: hotelName ?? "Atom" })}
+ {t("welcomeBackSub", { hotelName })}
diff --git a/src/app/(site)/logo/page.tsx b/src/app/(site)/logo/page.tsx
index cda9701a..d74f5b59 100644
--- a/src/app/(site)/logo/page.tsx
+++ b/src/app/(site)/logo/page.tsx
@@ -1,5 +1,6 @@
import LogoGenerator from "@/components/public/logo-generator";
import { ContentCard } from "@/components/public/ui";
+import { resolveHotelName } from "@/lib/hotel-name";
export const metadata = { title: "Logo generator" };
@@ -9,7 +10,8 @@ export const metadata = { title: "Logo generator" };
* client-side , which does all styling, live preview, and PNG
* export in the browser (no server data, no DB).
*/
-export default function LogoPage() {
+export default async function LogoPage() {
+ const initialText = await resolveHotelName();
return (
-
+
);
}
diff --git a/src/app/(site)/maintenance/page.tsx b/src/app/(site)/maintenance/page.tsx
index 118db2b6..c8149131 100644
--- a/src/app/(site)/maintenance/page.tsx
+++ b/src/app/(site)/maintenance/page.tsx
@@ -1,5 +1,6 @@
import { getTranslations } from "next-intl/server";
import { ContentCard } from "@/components/public/ui";
+import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300;
@@ -7,7 +8,7 @@ export const revalidate = 300;
export default async function MaintenancePage() {
const t = await getTranslations("pages.maintenance");
const [hotel, message] = await Promise.all([
- siteSettings.get("hotel_name", "Atom"),
+ resolveHotelName(),
siteSettings.get(
"maintenance_message",
"We are currently performing maintenance. Please check back soon.",
@@ -18,7 +19,7 @@ export default async function MaintenancePage() {
{message}
diff --git a/src/app/(site)/me/page.tsx b/src/app/(site)/me/page.tsx
index 6904b5c1..02e45cf3 100644
--- a/src/app/(site)/me/page.tsx
+++ b/src/app/(site)/me/page.tsx
@@ -6,6 +6,7 @@ import { claimReferral } from "@/actions/referral";
import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
import { auth } from "@/lib/auth";
import { avatarImageUrl } from "@/lib/format";
+import { resolveHotelName } from "@/lib/hotel-name";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
import CopyReferralButton from "./CopyReferralButton";
@@ -85,7 +86,7 @@ export default async function MePage({
"habbo_imaging_url",
"https://www.habbo.com/habbo-imaging/avatarimage",
),
- siteSettings.get("hotel_name", "Atom"),
+ resolveHotelName(),
prisma.websiteSetting
.findUnique({
where: { key: "referrals_needed" },
@@ -251,7 +252,7 @@ export default async function MePage({
height={22}
unoptimized
/>
- Enter {hotelName ?? "Hotel"}
+ Enter {hotelName}
{/* ── Stat tiles ── */}
diff --git a/src/app/(site)/page.tsx b/src/app/(site)/page.tsx
index 29f1c343..d1fab982 100644
--- a/src/app/(site)/page.tsx
+++ b/src/app/(site)/page.tsx
@@ -6,6 +6,7 @@ import { cached } from "@/lib/cache";
import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
+import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
import { auth } from "@/lib/auth";
@@ -13,7 +14,7 @@ export const revalidate = 120;
async function getHotelData() {
const [hotelName, imagerRaw] = await Promise.all([
- siteSettings.get("hotel_name", "Atom"),
+ resolveHotelName(),
siteSettings.get(
"habbo_imaging_url",
"https://www.habbo.com/habbo-imaging/avatarimage",
@@ -103,13 +104,13 @@ export default async function Home() {
- {th("online", { count: online, hotel: hotelName ?? "Atom" })}
+ {th("online", { count: online, hotel: hotelName })}
- {tp("welcome", { hotelName: hotelName ?? "Atom" })}
+ {tp("welcome", { hotelName })}
- {tp("joinNow", { hotelName: hotelName ?? "Atom" })}
+ {tp("joinNow", { hotelName })}
{tp("welcomeBody")}
diff --git a/src/app/(site)/register/page.tsx b/src/app/(site)/register/page.tsx
index ce066d36..0e42bd2c 100644
--- a/src/app/(site)/register/page.tsx
+++ b/src/app/(site)/register/page.tsx
@@ -7,13 +7,14 @@ import { cached } from "@/lib/cache";
import { avatarImageUrl } from "@/lib/format";
import { prisma } from "@/lib/prisma";
import { captchaConfig } from "@/lib/services/captcha";
+import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
export default async function RegisterPage() {
const th = await getTranslations("header");
const tpr = await getTranslations("pages.register");
const [hotelName, imagerRaw, cfg] = await Promise.all([
- siteSettings.get("hotel_name", "Hotel"),
+ resolveHotelName(),
siteSettings.get(
"habbo_imaging_url",
"https://www.habbo.com/habbo-imaging/avatarimage",
@@ -167,7 +168,7 @@ export default async function RegisterPage() {
v ?? "Atom"),
+ resolveHotelName(),
]);
return { articles, online, hotelName };
},
);
return apiJson(data);
} catch {
- return apiJson({ articles: [], online: 0, hotelName: "Atom" }, { status: 200 });
+ return apiJson({ articles: [], online: 0, hotelName: FALLBACK_HOTEL_NAME }, { status: 200 });
}
}
diff --git a/src/app/api/paypal/capture/route.ts b/src/app/api/paypal/capture/route.ts
index d541b891..6e2e2474 100644
--- a/src/app/api/paypal/capture/route.ts
+++ b/src/app/api/paypal/capture/route.ts
@@ -19,7 +19,7 @@ import {
} from "@/lib/services/paypal-topup";
import { rcon } from "@/lib/services/rcon";
import { sendCurrency } from "@/lib/services/send-currency";
-import { siteSettings } from "@/lib/services/site-settings";
+import { resolveHotelName } from "@/lib/hotel-name";
export const dynamic = "force-dynamic";
@@ -158,8 +158,7 @@ export async function POST(req: Request): Promise {
);
}
- const hotelName =
- (await siteSettings.get("hotel_name", env.HOTEL_NAME)) ?? env.HOTEL_NAME;
+ const hotelName = await resolveHotelName();
if (result.status !== "COMPLETED") {
// Record the non-completed attempt so support can trace it.
diff --git a/src/app/api/paypal/create/route.ts b/src/app/api/paypal/create/route.ts
index 6ac40e26..8be20f7a 100644
--- a/src/app/api/paypal/create/route.ts
+++ b/src/app/api/paypal/create/route.ts
@@ -11,7 +11,7 @@ import {
PAYPAL_CURRENCY,
} from "@/lib/services/paypal";
import { recordCreatedTopup } from "@/lib/services/paypal-topup";
-import { siteSettings } from "@/lib/services/site-settings";
+import { resolveHotelName } from "@/lib/hotel-name";
export const dynamic = "force-dynamic";
@@ -69,8 +69,7 @@ export async function POST(req: Request): Promise {
const credits = Math.floor(amount * creditsPerUnit());
const base = env.APP_URL.replace(/\/+$/, "");
- const hotelName =
- (await siteSettings.get("hotel_name", env.HOTEL_NAME)) ?? env.HOTEL_NAME;
+ const hotelName = await resolveHotelName();
try {
const order = await createOrder(amount, {
diff --git a/src/app/client/page.tsx b/src/app/client/page.tsx
index 5028b1fd..c84b10af 100644
--- a/src/app/client/page.tsx
+++ b/src/app/client/page.tsx
@@ -4,6 +4,7 @@ import { auth } from "@/lib/auth";
import { issueSsoTicket } from "@/lib/auth/sso-ticket";
import { cached } from "@/lib/cache";
import { prisma } from "@/lib/prisma";
+import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
import { ClientView } from "./client-view";
@@ -16,12 +17,12 @@ export default async function ClientPage() {
const userId = Number(session.user.id);
const [hotelName, clientUrl] = await Promise.all([
- siteSettings.get("hotel_name", "Atom"),
+ resolveHotelName(),
siteSettings.get("nitro_client_url", ""),
]);
const ip = (await headers()).get("x-real-client-ip") ?? "0.0.0.0";
- const ticket = await issueSsoTicket(prisma, userId, hotelName ?? "Atom", ip);
+ const ticket = await issueSsoTicket(prisma, userId, hotelName, ip);
const onlineCount = await cached("online_count", 10_000, () =>
prisma.user.count({ where: { online: "1" } }),
@@ -31,7 +32,7 @@ export default async function ClientPage() {
);
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f3aa9eea..dbccfa81 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -10,6 +10,7 @@ import { PwaRegister } from "@/components/pwa-register";
import { ThemeVars } from "@/components/theme-vars";
import { SmoothScroll } from "@/components/smooth-scroll";
import { enforceSiteAccess } from "@/lib/access-guard";
+import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
import "./globals.css";
@@ -29,7 +30,7 @@ const pixelFont = Pixelify_Sans({
export async function generateMetadata(): Promise {
const appUrl = process.env.APP_URL ?? "http://localhost:3000";
- const hotel = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
+ const hotel = await resolveHotelName();
const customFavicon = await siteSettings.get("cms_favicon");
const faviconUrl = customFavicon ?? "/favicon.svg";
const icons = [
diff --git a/src/app/manifest.ts b/src/app/manifest.ts
index b3d26ec3..884d1a84 100644
--- a/src/app/manifest.ts
+++ b/src/app/manifest.ts
@@ -1,11 +1,12 @@
import type { MetadataRoute } from "next";
+import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
// Web app manifest — makes the hotel installable as a PWA (AtomCMS exposed PWA
// settings but the rewrite ships a real, themeable manifest). Name + theme
// colour follow the live website_settings; falls back to defaults with no DB.
export default async function manifest(): Promise {
- const name = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
+ const name = await resolveHotelName();
const theme =
(await siteSettings.get("color_primary", "#f59e0b")) ?? "#f59e0b";
diff --git a/src/components/home/guest-view.tsx b/src/components/home/guest-view.tsx
index 971c560c..cf9fc3eb 100644
--- a/src/components/home/guest-view.tsx
+++ b/src/components/home/guest-view.tsx
@@ -11,7 +11,6 @@ import { siteSettings } from "@/lib/services/site-settings";
export default async function GuestView() {
const t = await getTranslations("pages.home");
- const _hotelName = await siteSettings.get("hotel_name", "Atom");
const imager =
(await siteSettings.get(
"habbo_imaging_url",
diff --git a/src/components/home/user-view.tsx b/src/components/home/user-view.tsx
index c7b55bc7..85dcc7ad 100644
--- a/src/components/home/user-view.tsx
+++ b/src/components/home/user-view.tsx
@@ -3,6 +3,7 @@ import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { Reveal } from "@/components/motion-reveal";
import { avatarImageUrl, excerpt } from "@/lib/format";
+import { resolveHotelName } from "@/lib/hotel-name";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
@@ -20,7 +21,7 @@ export default async function UserView({
motto,
}: UserViewProps) {
const t = await getTranslations("pages.home");
- const hotelName = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
+ const hotelName = await resolveHotelName();
const imager =
(await siteSettings.get(
"habbo_imaging_url",
diff --git a/src/components/mobile-nav.tsx b/src/components/mobile-nav.tsx
index d1a55c20..de76d8e1 100644
--- a/src/components/mobile-nav.tsx
+++ b/src/components/mobile-nav.tsx
@@ -3,6 +3,7 @@
import { AnimatePresence, motion } from "framer-motion";
import Image from "next/image";
import { type ReactNode, useRef, useState } from "react";
+import { FALLBACK_HOTEL_NAME } from "@/lib/brand";
import { mobileMenuVariants } from "@/lib/motion";
interface MobileNavProps {
@@ -16,7 +17,7 @@ export function MobileNav({
children,
menuLabel = "Open menu",
closeLabel = "Close menu",
- brandLabel = "Atom",
+ brandLabel = FALLBACK_HOTEL_NAME,
}: MobileNavProps) {
const [open, setOpen] = useState(false);
const detailsRef = useRef(null);
diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx
index 6b44aec4..57c3ba4b 100644
--- a/src/components/navigation.tsx
+++ b/src/components/navigation.tsx
@@ -3,6 +3,7 @@ import Link from "next/link";
import { getTranslations } from "next-intl/server";
import type { Session } from "next-auth";
import { LanguageSwitcher } from "@/components/language-switcher";
+import { resolveHotelName } from "@/lib/hotel-name";
import { MobileNav } from "@/components/mobile-nav";
import { NavDropdown } from "@/components/nav-dropdown";
import { NavbarColorPicker } from "@/components/navbar-color-picker";
@@ -10,6 +11,7 @@ import { ThemeSwitcher } from "@/components/theme-switcher";
export async function Navigation({ session }: { session: Session | null }) {
const t = await getTranslations("nav");
+ const brandLabel = await resolveHotelName();
const isStaff = (session?.user?.rank ?? 0) >= 7;
return (
@@ -170,6 +172,7 @@ export async function Navigation({ session }: { session: Session | null }) {
- {t("copyright", { year, hotel: hotelName ?? "Atom" })}
+ {t("copyright", { year, hotel: hotelName })}
diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx
index fd4bef38..8a07e8b0 100644
--- a/src/components/site-header.tsx
+++ b/src/components/site-header.tsx
@@ -3,12 +3,13 @@ import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { cached } from "@/lib/cache";
import { prisma } from "@/lib/prisma";
+import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
export async function SiteHeader() {
const t = await getTranslations("header");
const [hotelName, header, logo] = await Promise.all([
- siteSettings.get("hotel_name", "Atom"),
+ resolveHotelName(),
siteSettings.get("cms_header", "/assets/images/background.png"),
siteSettings.get("cms_logo", ""),
]);
@@ -77,7 +78,7 @@ export async function SiteHeader() {
- {t("online", { count: online, hotel: hotelName ?? "Atom" })}
+ {t("online", { count: online, hotel: hotelName })}
diff --git a/src/env.ts b/src/env.ts
index 598a3f82..c635a8b6 100644
--- a/src/env.ts
+++ b/src/env.ts
@@ -1,4 +1,5 @@
import { z } from "zod";
+import { FALLBACK_HOTEL_NAME } from "@/lib/brand";
// Minimal validated env for the foundation. When the Next.js app is added this
// will move to @t3-oss/env-nextjs (the habbo-next pattern), but the data layer
@@ -15,7 +16,7 @@ const schema = z.object({
.int()
.positive()
.default(10_000),
- HOTEL_NAME: z.string().default("Atom"),
+ HOTEL_NAME: z.string().default(FALLBACK_HOTEL_NAME),
APP_URL: z.string().url().default("http://localhost:3000"),
// Resend API key (preferred — simpler HTTP API, always works).
RESEND_API_KEY: z.string().optional(),
diff --git a/src/lib/brand.ts b/src/lib/brand.ts
new file mode 100644
index 00000000..8c3551be
--- /dev/null
+++ b/src/lib/brand.ts
@@ -0,0 +1,10 @@
+/**
+ * Single hardcoded fallback hotel brand.
+ * Override order at runtime:
+ * 1. website_settings.hotel_name
+ * 2. HOTEL_NAME env
+ * 3. this constant
+ *
+ * Safe for client components (no env / DB imports).
+ */
+export const FALLBACK_HOTEL_NAME = "Atom";
diff --git a/src/lib/hotel-name.ts b/src/lib/hotel-name.ts
new file mode 100644
index 00000000..53d15a97
--- /dev/null
+++ b/src/lib/hotel-name.ts
@@ -0,0 +1,26 @@
+import "server-only";
+
+import { env } from "@/env";
+import { FALLBACK_HOTEL_NAME } from "@/lib/brand";
+import { siteSettings } from "@/lib/services/site-settings";
+
+/**
+ * Resolve the public hotel name: CMS setting → HOTEL_NAME env → FALLBACK_HOTEL_NAME.
+ */
+export async function resolveHotelName(): Promise {
+ const fromSettings = await siteSettings.get("hotel_name", env.HOTEL_NAME);
+ const trimmed = fromSettings?.trim();
+ if (trimmed) return trimmed;
+ const fromEnv = env.HOTEL_NAME?.trim();
+ if (fromEnv) return fromEnv;
+ return FALLBACK_HOTEL_NAME;
+}
+
+/** Coalesce an already-fetched hotel name string. */
+export function coalesceHotelName(value: string | null | undefined): string {
+ const trimmed = value?.trim();
+ if (trimmed) return trimmed;
+ const fromEnv = env.HOTEL_NAME?.trim();
+ if (fromEnv) return fromEnv;
+ return FALLBACK_HOTEL_NAME;
+}
diff --git a/src/lib/services/site-settings.ts b/src/lib/services/site-settings.ts
index 1a07311c..93722dce 100644
--- a/src/lib/services/site-settings.ts
+++ b/src/lib/services/site-settings.ts
@@ -1,10 +1,11 @@
import "server-only";
+import { FALLBACK_HOTEL_NAME } from "@/lib/brand";
import { prisma } from "@/lib/prisma";
import { redis } from "@/lib/redis";
const DEFAULTS: Record = {
- hotel_name: "Atom",
+ hotel_name: FALLBACK_HOTEL_NAME,
habbo_imaging_url: "/imaging",
logo_url: "",
nitro_client_url: "",
diff --git a/tsconfig.json b/tsconfig.json
index 4e271d29..b7b56530 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -30,5 +30,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
- "exclude": ["node_modules"]
+ "exclude": ["node_modules", "e2e", "playwright.config.ts"]
}