refactor: add SubNav layout and per-subsite nav placeholders; switch Base to use SubNav

This commit is contained in:
2025-11-28 09:07:55 -05:00
parent de50889b2c
commit d8d6c5ec21
26 changed files with 1227 additions and 122 deletions

View File

@@ -6,10 +6,11 @@ interface Props {
}
import Themes from "astro-themes";
import { ViewTransitions } from "astro:transitions";
import BaseHead from "../components/BaseHead.astro";
import Navbar from "./Nav.astro";
import { metaData } from "../config";
import Nav from "./Nav.astro";
import SubNav from "./SubNav.astro";
import Footer from "../components/Footer.astro";
import "@fontsource/geist-sans/400.css";
@@ -19,21 +20,55 @@ import "@fontsource/geist-mono/600.css";
import "@styles/global.css";
import "@fonts/fonts.css";
const { title, description, image } = Astro.props;
const hostHeader = Astro.request?.headers?.get('host') || '';
const host = hostHeader.split(':')[0];
import { getSubsiteByHost, getSubsiteFromSignal } from '../utils/subsites.js';
// Determine if this request maps to a subsite (either via host or path)
// support legacy detection if path starts with /subsites/req — default host should match SUBSITES
// also support path-layout detection (e.g. /subsites/req)
import { getSubsiteByPath } from '../utils/subsites.js';
const detectedSubsite = getSubsiteByHost(host) ?? getSubsiteByPath(Astro.url.pathname) ?? null;
const isReq = detectedSubsite?.short === 'req';
const isReqSubdomain = host?.startsWith('req.');
import { WHITELABELS } from "../config";
// Accept forced whitelabel via query param, headers or request locals (set by middleware)
const forcedParam = Astro.url.searchParams.get('whitelabel') || Astro.request?.headers?.get('x-whitelabel') || (Astro.request as any)?.locals?.whitelabel || null;
let whitelabel: any = null;
if (forcedParam) {
const forced = getSubsiteFromSignal(forcedParam);
if (forced) whitelabel = WHITELABELS[forced.host] ?? null;
}
// fallback: by host mapping or legacy /subsites/req detection
if (!whitelabel) {
whitelabel = WHITELABELS[host] ?? (isReq ? WHITELABELS[detectedSubsite.host] : null);
}
// Determine whether we consider this request a subsite. Middleware will set
// request locals.isSubsite, which we trust here, but as a fallback also use
// the presence of a whitelabel mapping or a detected subsite path.
const isSubsite = (Astro.request as any)?.locals?.isSubsite ?? Boolean(whitelabel || detectedSubsite);
// Debug logging
console.log(`[Base.astro] host: ${host}, forcedParam: ${forcedParam}, isReq: ${isReq}, whitelabel: ${JSON.stringify(whitelabel)}`);
---
<html lang="en" class="scrollbar-hide lenis lenis-smooth">
<html lang="en" class="scrollbar-hide lenis lenis-smooth" data-subsite={isSubsite ? 'true' : 'false'}>
<head>
<ViewTransitions />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="googlebot"
content="index, follow, max-video-preview:-1, max-image-preview:large, max-snippet:-1"
/>
<Themes />
<BaseHead title={title} description={description} image={image} />
<BaseHead title={whitelabel?.siteTitle ?? title} description={description} image={image ?? metaData.ogImage} isWhitelabel={!!whitelabel} />
<!-- Subsite state is available on the html[data-subsite] attribute -->
<script>
import "@scripts/lenisSmoothScroll.js";
import "@scripts/main.jsx";
@@ -41,19 +76,23 @@ const { title, description, image } = Astro.props;
</head>
<body
class="antialiased flex flex-col items-center justify-center mx-auto mt-2 lg:mt-8 mb-20 lg:mb-40
scrollbar-hide">
scrollbar-hide"
style={`--brand-color: ${whitelabel?.brandColor ?? '#111827'}`}>
<main
class="flex-auto min-w-0 mt-2 md:mt-6 flex flex-col px-6 sm:px-4 md:px-0 max-w-3xl w-full">
class="page-enter flex-auto min-w-0 mt-2 md:mt-6 flex flex-col px-6 sm:px-4 md:px-0 max-w-3xl w-full">
<noscript>
<div style="background: #f44336; color: white; padding: 1em; text-align: center;">
This site requires JavaScript to function. Please enable JavaScript in your browser.
</div>
</noscript>
<Navbar />
{whitelabel ? <SubNav whitelabel={whitelabel} subsite={detectedSubsite} /> : <Nav />}
<slot />
<Footer />
</main>
<style>
/* Minimal page transition to replace deprecated ViewTransitions */
.page-enter { opacity: 0; transform: translateY(6px); transition: opacity 220ms ease, transform 240ms ease; }
html.page-ready .page-enter { opacity: 1; transform: none; }
/* CSS rules for the page scrollbar */
.scrollbar-hide::-webkit-scrollbar {
display: none;
@@ -64,5 +103,20 @@ const { title, description, image } = Astro.props;
scrollbar-width: none;
}
</style>
<script>
// Mark the page ready so CSS transitions run (replaces ViewTransitions).
// We don't rely on any Astro-specific API here — just a small client-side toggle.
(function () {
try {
// Add page-ready on the next animation frame so the transition always runs.
if (typeof window !== 'undefined') {
requestAnimationFrame(() => document.documentElement.classList.add('page-ready'));
}
} catch (e) {
// no-op
}
})();
</script>
</body>
</html>

View File

@@ -6,6 +6,12 @@ import { padlockIconSvg, userIconSvg, externalLinkIconSvg } from "@/utils/navAss
import "@/assets/styles/nav.css";
const user = await requireAuthHook(Astro);
const hostHeader = Astro.request?.headers?.get('host') || '';
const host = hostHeader.split(':')[0];
import { getSubsiteByHost } from '../utils/subsites.js';
import { getSubsiteByPath } from '../utils/subsites.js';
const isReq = getSubsiteByHost(host)?.short === 'req' || getSubsiteByPath(Astro.url.pathname)?.short === 'req';
// Nav is the standard site navigation — whitelabel logic belongs in SubNav
const isLoggedIn = Boolean(user);
const userDisplayName = user?.user ?? null;
@@ -44,11 +50,10 @@ const currentPath = Astro.url.pathname;
<div class="nav-bar-row flex items-center gap-4 justify-between">
<!-- Logo/Brand -->
<a
href="/"
class="text-xl sm:text-2xl font-semibold header-text whitespace-nowrap hover:opacity-80 transition-opacity"
>
{metaData.title}
</a>
href="/"
class="text-xl sm:text-2xl font-semibold header-text whitespace-nowrap hover:opacity-80 transition-opacity">
{metaData.title}
</a>
<!-- Desktop Navigation -->
<div class="desktop-nav flex items-center">
@@ -70,9 +75,10 @@ const currentPath = Astro.url.pathname;
<a
href={item.href}
class={isActive
? "flex items-center gap-0 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all duration-200 bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900"
? "flex items-center gap-0 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all duration-200 text-white"
: "flex items-center gap-0 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all duration-200 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800"
}
style={isActive ? `background: #111827` : undefined}
target={isExternal ? "_blank" : undefined}
rel={(isExternal || isAuthedPath) ? "external" : undefined}
onclick={item.onclick}
@@ -174,9 +180,10 @@ const currentPath = Astro.url.pathname;
<a
href={item.href}
class={isActive
? "flex items-center gap-0 px-4 py-3 rounded-lg text-base font-medium transition-all duration-200 bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900"
? "flex items-center gap-0 px-4 py-3 rounded-lg text-base font-medium transition-all duration-200 text-white"
: "flex items-center gap-0 px-4 py-3 rounded-lg text-base font-medium transition-all duration-200 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800"
}
style={isActive ? `background: #111827` : undefined}
target={isExternal ? "_blank" : undefined}
rel={(isExternal || isAuthedPath) ? "external" : undefined}
onclick={item.onclick}

11
src/layouts/SubNav.astro Normal file
View File

@@ -0,0 +1,11 @@
---
// Generic subsite navigation router — renders a per-subsite nav when available
// Props: whitelabel: object, subsite: { host, path, short }
const whitelabel = Astro.props?.whitelabel ?? null;
const subsite = Astro.props?.subsite ?? null;
import ReqSubNav from './subsites/reqNav.astro';
import DefaultSubNav from './subsites/defaultNav.astro';
---
{subsite?.short === 'req' ? <ReqSubNav whitelabel={whitelabel} /> : <DefaultSubNav whitelabel={whitelabel} />}

View File

@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
const WhitelabelLayout = ({ children, header, footer, customStyles }) => {
return (
<div style={customStyles} className="whitelabel-layout">
{header && <header className="whitelabel-header">{header}</header>}
<main className="whitelabel-main">{children}</main>
{footer && <footer className="whitelabel-footer">{footer}</footer>}
</div>
);
};
WhitelabelLayout.propTypes = {
children: PropTypes.node.isRequired,
header: PropTypes.node,
footer: PropTypes.node,
customStyles: PropTypes.object,
};
WhitelabelLayout.defaultProps = {
header: null,
footer: null,
customStyles: {},
};
export default WhitelabelLayout;

View File

@@ -0,0 +1,18 @@
---
// Default subsite nav — used when a subsite exists but no specialized nav is available
const whitelabel = Astro.props?.whitelabel ?? null;
const currentPath = Astro.url.pathname;
---
<nav class="w-full px-4 sm:px-6 py-4 bg-transparent sticky top-0 z-50 backdrop-blur-sm bg-white/80 dark:bg-[#121212]/80 border-b border-neutral-200/50 dark:border-neutral-800/50">
<div class="max-w-7xl mx-auto flex items-center justify-between">
<a href="/" class="text-xl sm:text-2xl font-semibold" style={`color: ${whitelabel?.brandColor ?? 'var(--brand-color)'}`}>
{whitelabel?.logoText ?? 'Subsite'}
</a>
<div class="flex items-center gap-4">
<!-- placeholder for future global subsite nav items -->
<button aria-label="Toggle theme" type="button" class="flex items-center justify-center w-8 h-8 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors" onclick="toggleTheme()">
</button>
</div>
</div>
</nav>

View File

@@ -0,0 +1,26 @@
---
// Req specific subsite nav placeholder. Keeps markup minimal for now.
import { Icon } from "astro-icon/components";
const whitelabel = Astro.props?.whitelabel ?? null;
const currentPath = Astro.url.pathname;
const links = [
// Add req-specific nav items here in future
];
---
<nav class="w-full px-4 sm:px-6 py-4 bg-transparent sticky top-0 z-50 backdrop-blur-sm bg-white/80 dark:bg-[#121212]/80 border-b border-neutral-200/50 dark:border-neutral-800/50">
<div class="max-w-7xl mx-auto flex items-center justify-between">
<a href="/" class="text-xl sm:text-2xl font-semibold" style={`color: ${whitelabel?.brandColor ?? 'var(--brand-color)'}`}>
{whitelabel?.logoText ?? 'REQ'}
</a>
<ul class="flex items-center gap-4">
<!-- currently empty; future subsite-specific links go here -->
<li>
<button aria-label="Toggle theme" type="button" class="flex items-center justify-center w-8 h-8 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors" onclick="toggleTheme()">
<Icon name="fa6-solid:circle-half-stroke" class="h-4 w-4 text-[#1c1c1c] dark:text-[#D4D4D4]" />
</button>
</li>
</ul>
</div>
</nav>