refactor: add SubNav layout and per-subsite nav placeholders; switch Base to use SubNav
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
11
src/layouts/SubNav.astro
Normal 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} />}
|
||||
27
src/layouts/WhitelabelLayout.jsx
Normal file
27
src/layouts/WhitelabelLayout.jsx
Normal 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;
|
||||
18
src/layouts/subsites/defaultNav.astro
Normal file
18
src/layouts/subsites/defaultNav.astro
Normal 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>
|
||||
26
src/layouts/subsites/reqNav.astro
Normal file
26
src/layouts/subsites/reqNav.astro
Normal 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>
|
||||
Reference in New Issue
Block a user