diff --git a/public/styles/player.css b/public/styles/player.css
new file mode 100644
index 0000000..c71ff3c
--- /dev/null
+++ b/public/styles/player.css
@@ -0,0 +1,86 @@
+/* Universal box-sizing for consistency */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+:root {
+ --lrc-text-color: #333; /* darker text */
+ --lrc-bg-color: rgba(0, 0, 0, 0.05);
+ --lrc-active-color: #000; /* bold black for active */
+ --lrc-active-shadow: none; /* no glow in light mode */
+ --lrc-hover-color: #005fcc; /* darker blue hover */
+}
+
+[data-theme="dark"] {
+ --lrc-text-color: #ccc; /* original gray */
+ --lrc-bg-color: rgba(255, 255, 255, 0.02);
+ --lrc-active-color: #fff; /* bright white for active */
+ --lrc-active-shadow: 0 0 4px rgba(212, 175, 55, 0.6); /* gold glow */
+ --lrc-hover-color: #4fa2ff; /* original blue hover */
+}
+
+body {
+ font-family: sans-serif;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ min-width: 100vw;
+ min-height: 100vh;
+ /* background: linear-gradient(-45deg, #FFCDD2 50%, #B2EBF2 50%); */
+}
+
+/* Container for the player and album cover */
+.music-container {
+ width: 800px; /* fixed desktop width */
+ max-width: 90vw; /* prevent overflow on smaller screens */
+ height: auto !important;
+ margin: 0 auto 120px auto; /* increased bottom margin */
+ overflow-x: visible; /* allow horizontal overflow if needed */
+ overflow-y: hidden;
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ box-sizing: border-box;
+ padding: 1rem;
+ box-shadow: 1px 1px 5px 0 rgba(0, 0, 0, 0.3);
+}
+
+/* Album cover section */
+.album-cover {
+ aspect-ratio: 1 / 1;
+ width: 100%;
+ max-width: 30%;
+ height: auto;
+}
+
+.album-cover > img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+
+.album-cover > img:hover {
+ opacity: 0.7;
+}
+
+/* Player info and controls */
+.music-player {
+ flex: 1 1 70%; /* Take remaining ~70% */
+ max-width: 70%;
+ width: auto;
+ height: auto !important; /* Match container height */
+ padding: 1em;
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ background: inherit;
+ box-sizing: border-box;
+ min-width: 0; /* Fix flex overflow */
+ flex-shrink: 0; /* Prevent shrinking that hides content */
+}
diff --git a/src/assets/styles/player.css b/src/assets/styles/player.css
index 72ae265..83a2bee 100644
--- a/src/assets/styles/player.css
+++ b/src/assets/styles/player.css
@@ -1,36 +1,6 @@
-/* Universal box-sizing for consistency */
-*,
-*::before,
-*::after {
- box-sizing: border-box;
-}
+/* player.css moved to /public/styles/player.css — kept empty here to avoid
+ accidental inclusion by the build tool during development. */
-:root {
- --lrc-text-color: #333; /* darker text */
- --lrc-bg-color: rgba(0, 0, 0, 0.05);
- --lrc-active-color: #000; /* bold black for active */
- --lrc-active-shadow: none; /* no glow in light mode */
- --lrc-hover-color: #005fcc; /* darker blue hover */
-}
-
-[data-theme="dark"] {
- --lrc-text-color: #ccc; /* original gray */
- --lrc-bg-color: rgba(255, 255, 255, 0.02);
- --lrc-active-color: #fff; /* bright white for active */
- --lrc-active-shadow: 0 0 4px rgba(212, 175, 55, 0.6); /* gold glow */
- --lrc-hover-color: #4fa2ff; /* original blue hover */
-}
-
-body {
- font-family: sans-serif;
- width: 100%;
- height: 100%;
- margin: 0;
- padding: 0;
- min-width: 100vw;
- min-height: 100vh;
- /* background: linear-gradient(-45deg, #FFCDD2 50%, #B2EBF2 50%); */
-}
/* Container for the player and album cover */
.music-container {
diff --git a/src/components/AppLayout.jsx b/src/components/AppLayout.jsx
index 9f0dbcc..0e73510 100644
--- a/src/components/AppLayout.jsx
+++ b/src/components/AppLayout.jsx
@@ -1,4 +1,4 @@
-import React, { Suspense, lazy } from 'react';
+import React, { Suspense, lazy, useState, useMemo, useEffect } from 'react';
import Memes from './Memes.jsx';
import Lighting from './Lighting.jsx';
import { toast } from 'react-toastify';
@@ -14,13 +14,69 @@ const LoginPage = lazy(() => import('./Login.jsx'));
const LyricSearch = lazy(() => import('./LyricSearch'));
const MediaRequestForm = lazy(() => import('./TRip/MediaRequestForm.jsx'));
const RequestManagement = lazy(() => import('./TRip/RequestManagement.jsx'));
-const Player = lazy(() => import('./AudioPlayer.jsx'));
+// NOTE: Player is intentionally NOT imported at module initialization.
+// We create the lazy import inside the component at render-time only when
+// we are on the main site and the Player island should be rendered. This
+// prevents bundling the player island into pages that are explicitly
+// identified as subsites.
+const ReqForm = lazy(() => import('./req/ReqForm.jsx'));
export default function Root({ child, user = undefined, ...props }) {
window.toast = toast;
const theme = document.documentElement.getAttribute("data-theme")
const loggedIn = props.loggedIn ?? Boolean(user);
usePrimeReactThemeSwitcher(theme);
+ // Avoid adding the Player island for subsite requests. We expose a
+ // runtime flag `window.__IS_SUBSITE` from the server layout so pages
+ // don't need to pass guards.
+ const isSubsite = typeof document !== 'undefined' && document.documentElement.getAttribute('data-subsite') === 'true';
+ // Helpful runtime debugging: only log when child changes so we don't spam
+ // the console on every render. Use an effect so output is stable.
+ useEffect(() => {
+ try {
+ if (typeof console !== 'undefined' && typeof document !== 'undefined') {
+ console.debug(`[AppLayout] child=${String(child)}, data-subsite=${document.documentElement.getAttribute('data-subsite')}`);
+ }
+ } catch (e) {
+ // no-op
+ }
+ }, [child]);
+
+ // Only initialize the lazy player when this is NOT a subsite and the
+ // active child is the Player island. Placing the lazy() call here
+ // avoids creating a static dependency at module load time.
+ // Create the lazy component only when we actually need it. Using
+ // `useMemo` ensures we don't re-create the lazy factory on every render
+ // which would create a new component identity and cause mount/unmount
+ // loops and repeated log messages.
+ const wantPlayer = !isSubsite && child === "Player";
+
+ // Use dynamic import+state on the client to avoid React.lazy identity
+ // churn and to surface any import-time errors. Since Root is used via
+ // client:only, this code runs in the browser and can safely import.
+ const [PlayerComp, setPlayerComp] = useState(null);
+ useEffect(() => {
+ let mounted = true;
+ if (wantPlayer) {
+ try { console.debug('[AppLayout] dynamic-import: requesting AudioPlayer'); } catch (e) { }
+ import('./AudioPlayer.jsx')
+ .then((mod) => {
+ if (!mounted) return;
+ // set the component factory
+ setPlayerComp(() => mod.default ?? null);
+ try { console.debug('[AppLayout] AudioPlayer import succeeded'); } catch (e) { }
+ })
+ .catch((err) => {
+ console.error('[AppLayout] AudioPlayer import failed', err);
+ if (mounted) setPlayerComp(() => null);
+ });
+ } else {
+ // unload if we no longer want the player
+ setPlayerComp(() => null);
+ }
+ return () => { mounted = false; };
+ }, [wantPlayer]);
+
return (
*/}
{child == "LoginPage" && ()}
{child == "LyricSearch" && ()}
- {child == "Player" && ()}
+ {child == "Player" && !isSubsite && PlayerComp && (
+ Loading player...}>
+
+
+ )}
{child == "Memes" && }
{child == "qs2.MediaRequestForm" && }
{child == "qs2.RequestManagement" && }
+ {child == "ReqForm" && }
{child == "Lighting" && }
diff --git a/src/components/AudioPlayer.jsx b/src/components/AudioPlayer.jsx
index 7844f71..32da0be 100644
--- a/src/components/AudioPlayer.jsx
+++ b/src/components/AudioPlayer.jsx
@@ -2,7 +2,10 @@ import React, { useState, useEffect, useRef, Suspense, lazy, useMemo, useCallbac
import { metaData } from "../config";
import Play from "@mui/icons-material/PlayArrow";
import Pause from "@mui/icons-material/Pause";
-import "@styles/player.css";
+// Load AudioPlayer CSS at runtime only when the player mounts on the client.
+// This avoids including the stylesheet in pages where the AudioPlayer never
+// gets loaded (subsidiary/whitelabel sites). We dynamically import the
+// stylesheet inside a client-only effect below.
import { Dialog } from "primereact/dialog";
import { AutoComplete } from "primereact/autocomplete";
import { DataTable } from "primereact/datatable";
@@ -48,6 +51,31 @@ const STATIONS = {
export default function Player({ user }) {
+ // Log lifecycle so we can confirm the player mounted in the browser
+ useEffect(() => {
+ // Load the player stylesheet from /public/styles/player.css so it will be
+ // requested at runtime only when the AudioPlayer mounts. This prevents the
+ // CSS from being included as a route asset for subsites.
+ try {
+ if (typeof window !== 'undefined') {
+ if (!document.getElementById('audio-player-css')) {
+ const link = document.createElement('link');
+ link.id = 'audio-player-css';
+ link.rel = 'stylesheet';
+ link.href = '/styles/player.css';
+ link.onload = () => { try { console.debug('[AudioPlayer] CSS loaded (link)'); } catch (e) { } };
+ link.onerror = (err) => { console.warn('[AudioPlayer] CSS link failed', err); };
+ document.head.appendChild(link);
+ }
+ }
+ } catch (e) {
+ // ignore
+ }
+ try { console.debug('[AudioPlayer] mounted'); } catch (e) { }
+ return () => {
+ try { console.debug('[AudioPlayer] unmounted'); } catch (e) { }
+ };
+ }, []);
// Inject custom paginator styles
useEffect(() => {
const styleId = 'queue-paginator-styles';
diff --git a/src/components/BaseHead.astro b/src/components/BaseHead.astro
index 8a5863a..022b496 100644
--- a/src/components/BaseHead.astro
+++ b/src/components/BaseHead.astro
@@ -3,6 +3,7 @@ interface Props {
title?: string;
description?: string;
image?: string;
+ isWhitelabel?: boolean;
}
import { metaData } from "../config";
@@ -11,21 +12,23 @@ import { JoyUIRootIsland } from "./Components"
import { useHtmlThemeAttr } from "../hooks/useHtmlThemeAttr";
import { usePrimeReactThemeSwitcher } from "../hooks/usePrimeReactThemeSwitcher";
-const { title, description, image } = Astro.props;
+const { title, description, image, isWhitelabel } = Astro.props;
const { url } = Astro;
-const shareTitle = title ? `${title} | ${metaData.title}` : metaData.shareTitle ?? metaData.title;
+const trimmedTitle = title?.trim();
+const seoTitle = trimmedTitle || metaData.title;
+const shareTitle = isWhitelabel ? (trimmedTitle || (metaData.shareTitle ?? metaData.title)) : (trimmedTitle ? `${trimmedTitle} | ${metaData.title}` : (metaData.shareTitle ?? metaData.title));
+const seoTitleTemplate = isWhitelabel ? "%s" : (trimmedTitle ? `%s | ${metaData.title}` : "%s");
const shareDescription = description ?? metaData.shareDescription ?? metaData.description;
const canonicalUrl = url?.href ?? metaData.baseUrl;
const shareImage = new URL(image ?? metaData.ogImage, metaData.baseUrl).toString();
const shareImageAlt = metaData.shareImageAlt ?? metaData.shareTitle ?? metaData.title;
---
-