2025-06-18 07:46:59 -04:00
|
|
|
---
|
|
|
|
|
interface Props {
|
|
|
|
|
title?: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
image?: string;
|
2025-11-28 09:07:55 -05:00
|
|
|
isWhitelabel?: boolean;
|
2025-12-02 10:05:43 -05:00
|
|
|
whitelabel?: any;
|
|
|
|
|
subsite?: any;
|
2025-06-18 07:46:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
import { metaData } from "../config";
|
|
|
|
|
import { SEO } from "astro-seo";
|
|
|
|
|
import { JoyUIRootIsland } from "./Components"
|
2025-07-17 06:55:01 -04:00
|
|
|
import { useHtmlThemeAttr } from "../hooks/useHtmlThemeAttr";
|
2025-07-16 10:06:41 -04:00
|
|
|
import { usePrimeReactThemeSwitcher } from "../hooks/usePrimeReactThemeSwitcher";
|
|
|
|
|
|
2025-12-02 10:05:43 -05:00
|
|
|
const { title, description, image, isWhitelabel, whitelabel, subsite } = Astro.props;
|
2025-06-18 07:46:59 -04:00
|
|
|
|
2025-11-26 14:42:57 -05:00
|
|
|
const { url } = Astro;
|
|
|
|
|
|
2025-11-28 09:07:55 -05:00
|
|
|
const trimmedTitle = title?.trim();
|
|
|
|
|
const seoTitle = trimmedTitle || metaData.title;
|
2025-12-02 10:05:43 -05:00
|
|
|
// If a whitelabel/subsite override exists, prefer its shareTitle/shareDescription/ogImage/favicon
|
|
|
|
|
const subsiteMeta = whitelabel ?? {};
|
|
|
|
|
const shareTitle = isWhitelabel
|
|
|
|
|
? (trimmedTitle || (subsiteMeta.shareTitle ?? metaData.shareTitle ?? metaData.title))
|
|
|
|
|
: (trimmedTitle ? `${trimmedTitle} | ${metaData.title}` : (metaData.shareTitle ?? metaData.title));
|
2025-11-28 09:07:55 -05:00
|
|
|
const seoTitleTemplate = isWhitelabel ? "%s" : (trimmedTitle ? `%s | ${metaData.title}` : "%s");
|
2025-12-02 10:05:43 -05:00
|
|
|
const shareDescription = isWhitelabel
|
|
|
|
|
? (trimmedTitle || (subsiteMeta.shareDescription ?? metaData.shareDescription ?? metaData.description))
|
|
|
|
|
: (description ?? metaData.shareDescription ?? metaData.description);
|
|
|
|
|
// Compute canonical URL with these priorities:
|
|
|
|
|
// 1. If the whitelabel/subsite provides a baseUrl, use that as the host for the canonical URL
|
|
|
|
|
// 2. If a subsite was detected and the request host matches the subsite host, canonical uses that host
|
|
|
|
|
// 3. If the request is for a path-based subsite (host is main site), prefer metaData.baseUrl so canonical remains on main site
|
|
|
|
|
// 4. Fallback to the request URL or metaData.baseUrl
|
|
|
|
|
const currentHost = (Astro.request?.headers?.get('host') || '').split(':')[0];
|
|
|
|
|
let canonicalBase: string | null = null;
|
|
|
|
|
if (subsiteMeta?.baseUrl) {
|
|
|
|
|
canonicalBase = subsiteMeta.baseUrl;
|
|
|
|
|
} else if (subsite?.host) {
|
|
|
|
|
// normalize hosts for comparison
|
|
|
|
|
const requestedHost = (currentHost || '').toLowerCase();
|
|
|
|
|
const subsiteHost = String(subsite.host || '').toLowerCase();
|
|
|
|
|
if (requestedHost && requestedHost === subsiteHost) {
|
|
|
|
|
canonicalBase = `https://${subsite.host}`;
|
|
|
|
|
} else {
|
|
|
|
|
// keep canonical on the main configured base (path-based subsites should remain under metaData.baseUrl)
|
|
|
|
|
canonicalBase = metaData.baseUrl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Decide whether canonical should be the site-root (e.g. https://req.boatson.boats/)
|
|
|
|
|
// or include a path. Rules:
|
|
|
|
|
// - If canonicalBase comes from a whitelabel/baseUrl or request host matches subsite host -> prefer site-root.
|
|
|
|
|
// - Otherwise (path-based subsites), include the pathname/search so canonical remains under the main site path.
|
|
|
|
|
const isHostMatchedSubsite = Boolean(subsite?.host && currentHost && currentHost.toLowerCase() === String(subsite.host).toLowerCase());
|
|
|
|
|
const isSubsiteRootCanonical = Boolean(subsiteMeta?.baseUrl || isHostMatchedSubsite);
|
|
|
|
|
let canonicalUrl: string;
|
|
|
|
|
if (canonicalBase) {
|
|
|
|
|
if (isSubsiteRootCanonical) {
|
|
|
|
|
// ensure canonicalBase ends with a single '/'
|
|
|
|
|
canonicalUrl = canonicalBase.endsWith('/') ? canonicalBase : `${canonicalBase}/`;
|
|
|
|
|
} else {
|
|
|
|
|
canonicalUrl = new URL((url?.pathname ?? '') + (url?.search ?? ''), canonicalBase).toString();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
canonicalUrl = url?.href ?? metaData.baseUrl;
|
|
|
|
|
}
|
|
|
|
|
// Prefer the whitelabel/subsite ogImage when this page is for a whitelabel site.
|
|
|
|
|
// Otherwise fall back to an explicit image prop or the global ogImage.
|
|
|
|
|
const resolvedOgImage = (isWhitelabel && subsiteMeta.ogImage) ? subsiteMeta.ogImage : (image ?? metaData.ogImage);
|
|
|
|
|
|
|
|
|
|
// Keep relative/site-root paths as-is (e.g. '/images/req.png') and don't force
|
|
|
|
|
// an absolute URL using metaData.baseUrl. Only keep absolute (http/https)
|
|
|
|
|
// URLs if the value is already absolute.
|
|
|
|
|
function keepRelativeOrAbsolute(val) {
|
|
|
|
|
if (!val) return val;
|
|
|
|
|
if (/^https?:\/\//i.test(val)) return val; // already absolute
|
|
|
|
|
// Normalize to site-root-relative so local testing always resolves under '/'
|
|
|
|
|
return val.startsWith('/') ? val : `/${val}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const shareImage = keepRelativeOrAbsolute(resolvedOgImage);
|
|
|
|
|
const shareImageAlt = subsiteMeta.shareImageAlt ?? metaData.shareImageAlt ?? metaData.shareTitle ?? metaData.title;
|
|
|
|
|
|
|
|
|
|
// Build icon links: prefer subsite icons when present. If a subsite provides a single
|
|
|
|
|
// `favicon` but no `icons` array, do NOT append the global icons to avoid duplicates.
|
|
|
|
|
const primaryIconHref = keepRelativeOrAbsolute(subsiteMeta.favicon ?? metaData.favicon);
|
|
|
|
|
// Ensure extraIcons are used only when subsite doesn't provide a single favicon (prevents duplicates)
|
|
|
|
|
const extraIcons = subsiteMeta.icons ? subsiteMeta.icons : (subsiteMeta.favicon ? [] : (metaData.icons ?? []));
|
2025-06-18 07:46:59 -04:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
<SEO
|
2025-11-28 09:07:55 -05:00
|
|
|
title={seoTitle}
|
|
|
|
|
titleTemplate={seoTitleTemplate}
|
2025-06-18 07:46:59 -04:00
|
|
|
titleDefault={metaData.title}
|
2025-12-02 10:05:43 -05:00
|
|
|
canonical={canonicalUrl}
|
2025-11-26 14:42:57 -05:00
|
|
|
description={shareDescription}
|
2025-06-18 07:46:59 -04:00
|
|
|
charset="UTF-8"
|
|
|
|
|
openGraph={{
|
|
|
|
|
basic: {
|
2025-11-26 14:42:57 -05:00
|
|
|
title: shareTitle,
|
2025-06-18 07:46:59 -04:00
|
|
|
type: "website",
|
2025-11-26 14:42:57 -05:00
|
|
|
image: shareImage,
|
|
|
|
|
url: canonicalUrl,
|
2025-06-18 07:46:59 -04:00
|
|
|
},
|
|
|
|
|
optional: {
|
2025-11-26 14:42:57 -05:00
|
|
|
description: shareDescription,
|
2025-12-02 10:05:43 -05:00
|
|
|
siteName: subsiteMeta.siteName ?? metaData.name,
|
2025-06-18 07:46:59 -04:00
|
|
|
locale: "en_US",
|
|
|
|
|
},
|
|
|
|
|
}}
|
2025-12-02 10:05:43 -05:00
|
|
|
extend={{
|
2025-11-26 14:42:57 -05:00
|
|
|
link: [
|
2025-12-02 10:05:43 -05:00
|
|
|
// choose subsite favicon if provided, else global config. Allow absolute or relative paths
|
|
|
|
|
{ rel: 'icon', href: primaryIconHref },
|
|
|
|
|
// additional icon links from config if present
|
|
|
|
|
...extraIcons,
|
2025-11-26 14:42:57 -05:00
|
|
|
],
|
|
|
|
|
meta: [
|
|
|
|
|
{ property: "og:image:alt", content: shareImageAlt },
|
|
|
|
|
{ name: "twitter:card", content: "summary_large_image" },
|
|
|
|
|
{ name: "twitter:title", content: shareTitle },
|
|
|
|
|
{ name: "twitter:description", content: shareDescription },
|
|
|
|
|
{ name: "twitter:image", content: shareImage },
|
|
|
|
|
{ name: "twitter:image:alt", content: shareImageAlt },
|
|
|
|
|
],
|
2025-06-18 07:46:59 -04:00
|
|
|
}}
|
2025-07-16 10:06:41 -04:00
|
|
|
/>
|
|
|
|
|
|